aboutsummaryrefslogtreecommitdiffstats
path: root/contracts/protocol
diff options
context:
space:
mode:
authorLeonid Logvinov <logvinov.leon@gmail.com>2018-12-11 06:37:48 +0800
committerFred Carlsen <fred@sjelfull.no>2018-12-13 01:19:33 +0800
commit2c58b8a88650fcc4f3622546c02a14bbf211cd07 (patch)
tree9c4dd90c688a5b102f9245d9f95d0e71ad1f31d2 /contracts/protocol
parentcb53cd05e62b1904184ec14862e48383f80b9c54 (diff)
downloaddexon-sol-tools-2c58b8a88650fcc4f3622546c02a14bbf211cd07.tar
dexon-sol-tools-2c58b8a88650fcc4f3622546c02a14bbf211cd07.tar.gz
dexon-sol-tools-2c58b8a88650fcc4f3622546c02a14bbf211cd07.tar.bz2
dexon-sol-tools-2c58b8a88650fcc4f3622546c02a14bbf211cd07.tar.lz
dexon-sol-tools-2c58b8a88650fcc4f3622546c02a14bbf211cd07.tar.xz
dexon-sol-tools-2c58b8a88650fcc4f3622546c02a14bbf211cd07.tar.zst
dexon-sol-tools-2c58b8a88650fcc4f3622546c02a14bbf211cd07.zip
Rename core package to protocol
Diffstat (limited to 'contracts/protocol')
-rw-r--r--contracts/protocol/.solhint.json20
-rw-r--r--contracts/protocol/CHANGELOG.json121
-rw-r--r--contracts/protocol/README.md74
-rw-r--r--contracts/protocol/compiler.json34
-rw-r--r--contracts/protocol/contracts/protocol/AssetProxy/ERC20Proxy.sol184
-rw-r--r--contracts/protocol/contracts/protocol/AssetProxy/ERC721Proxy.sol171
-rw-r--r--contracts/protocol/contracts/protocol/AssetProxy/MixinAuthorizable.sol117
-rw-r--r--contracts/protocol/contracts/protocol/AssetProxy/MultiAssetProxy.sol300
-rw-r--r--contracts/protocol/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol41
-rw-r--r--contracts/protocol/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol108
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/Exchange.sol53
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol174
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/MixinExchangeCore.sol529
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/MixinMatchOrders.sol335
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/MixinSignatureValidator.sol324
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/MixinTransactions.sol152
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/MixinWrapperFunctions.sol426
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol45
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/mixins/MExchangeCore.sol157
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/mixins/MMatchOrders.sol58
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/mixins/MSignatureValidator.sol75
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/mixins/MTransactions.sol58
-rw-r--r--contracts/protocol/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol41
-rw-r--r--contracts/protocol/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol37
-rw-r--r--contracts/protocol/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol58
-rw-r--r--contracts/protocol/contracts/test/TestExchangeInternals/TestExchangeInternals.sol191
-rw-r--r--contracts/protocol/contracts/test/TestSignatureValidator/TestSignatureValidator.sol45
-rw-r--r--contracts/protocol/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol81
-rw-r--r--contracts/protocol/package.json96
-rw-r--r--contracts/protocol/src/artifacts/index.ts27
-rw-r--r--contracts/protocol/src/index.ts3
-rw-r--r--contracts/protocol/src/wrappers/index.ts10
-rw-r--r--contracts/protocol/test/asset_proxy/authorizable.ts211
-rw-r--r--contracts/protocol/test/asset_proxy/proxies.ts1253
-rw-r--r--contracts/protocol/test/exchange/core.ts1178
-rw-r--r--contracts/protocol/test/exchange/dispatcher.ts284
-rw-r--r--contracts/protocol/test/exchange/fill_order.ts314
-rw-r--r--contracts/protocol/test/exchange/internal.ts472
-rw-r--r--contracts/protocol/test/exchange/match_orders.ts1284
-rw-r--r--contracts/protocol/test/exchange/signature_validator.ts526
-rw-r--r--contracts/protocol/test/exchange/transactions.ts466
-rw-r--r--contracts/protocol/test/exchange/wrapper.ts1461
-rw-r--r--contracts/protocol/test/global_hooks.ts17
-rw-r--r--contracts/protocol/test/multisig/asset_proxy_owner.ts506
-rw-r--r--contracts/protocol/test/utils/asset_proxy_owner_wrapper.ts70
-rw-r--r--contracts/protocol/test/utils/asset_wrapper.ts222
-rw-r--r--contracts/protocol/test/utils/erc20_wrapper.ts179
-rw-r--r--contracts/protocol/test/utils/erc721_wrapper.ts236
-rw-r--r--contracts/protocol/test/utils/exchange_wrapper.ts281
-rw-r--r--contracts/protocol/test/utils/fill_order_combinatorial_utils.ts928
-rw-r--r--contracts/protocol/test/utils/index.ts3
-rw-r--r--contracts/protocol/test/utils/match_order_tester.ts562
-rw-r--r--contracts/protocol/test/utils/order_factory_from_scenario.ts294
-rw-r--r--contracts/protocol/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts19
-rw-r--r--contracts/protocol/test/utils/simple_order_filled_cancelled_fetcher.ts31
-rw-r--r--contracts/protocol/tsconfig.json23
-rw-r--r--contracts/protocol/tslint.json6
57 files changed, 14971 insertions, 0 deletions
diff --git a/contracts/protocol/.solhint.json b/contracts/protocol/.solhint.json
new file mode 100644
index 000000000..076afe9f3
--- /dev/null
+++ b/contracts/protocol/.solhint.json
@@ -0,0 +1,20 @@
+{
+ "extends": "default",
+ "rules": {
+ "avoid-low-level-calls": false,
+ "avoid-tx-origin": "warn",
+ "bracket-align": false,
+ "code-complexity": false,
+ "const-name-snakecase": "error",
+ "expression-indent": "error",
+ "function-max-lines": false,
+ "func-order": "error",
+ "indent": ["error", 4],
+ "max-line-length": ["warn", 160],
+ "no-inline-assembly": false,
+ "quotes": ["error", "double"],
+ "separate-by-one-line-in-contract": "error",
+ "space-after-comma": "error",
+ "statement-indent": "error"
+ }
+}
diff --git a/contracts/protocol/CHANGELOG.json b/contracts/protocol/CHANGELOG.json
new file mode 100644
index 000000000..371f18cd4
--- /dev/null
+++ b/contracts/protocol/CHANGELOG.json
@@ -0,0 +1,121 @@
+[
+ {
+ "name": "MultiAssetProxy",
+ "version": "1.0.0",
+ "changes": [
+ {
+ "note": "Add MultiAssetProxy implementation",
+ "pr": 1224
+ }
+ ]
+ },
+ {
+ "name": "OrderValidator",
+ "version": "1.0.1",
+ "changes": [
+ {
+ "note": "remove `getApproved` check from ERC721 approval query",
+ "pr": 1149
+ }
+ ]
+ },
+ {
+ "name": "Forwarder",
+ "version": "1.1.0",
+ "changes": [
+ {
+ "note": "Round up when calculating remaining amounts in marketBuy functions",
+ "pr": 1162,
+ "networks": {
+ "1": "0x5468a1dc173652ee28d249c271fa9933144746b1",
+ "3": "0x2240dab907db71e64d3e0dba4800c83b5c502d4e",
+ "42": "0x17992e4ffb22730138e4b62aaa6367fa9d3699a6"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Forwarder",
+ "version": "1.0.0",
+ "changes": [
+ {
+ "note": "protocol v2 deploy",
+ "networks": {
+ "1": "0x7afc2d5107af94c462a194d2c21b5bdd238709d6",
+ "3": "0x3983e204b12b3c02fb0638caf2cd406a62e0ead3",
+ "42": "0xd85e2fa7e7e252b27b01bf0d65c946959d2f45b8"
+ }
+ }
+ ]
+ },
+ {
+ "name": "OrderValidator",
+ "version": "1.0.0",
+ "changes": [
+ {
+ "note": "protocol v2 deploy",
+ "networks": {
+ "1": "0x9463e518dea6810309563c81d5266c1b1d149138",
+ "3": "0x90431a90516ab49af23a0530e04e8c7836e7122f",
+ "42": "0xb389da3d204b412df2f75c6afb3d0a7ce0bc283d"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Exchange",
+ "version": "2.0.0",
+ "changes": [
+ {
+ "note": "protocol v2 deploy",
+ "networks": {
+ "1": "0x4f833a24e1f95d70f028921e27040ca56e09ab0b",
+ "3": "0x4530c0483a1633c7a1c97d2c53721caff2caaaaf",
+ "42": "0x35dd2932454449b14cee11a94d3674a936d5d7b2"
+ }
+ }
+ ]
+ },
+ {
+ "name": "ERC20Proxy",
+ "version": "1.0.0",
+ "changes": [
+ {
+ "note": "protocol v2 deploy",
+ "networks": {
+ "1": "0x2240dab907db71e64d3e0dba4800c83b5c502d4e",
+ "3": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa",
+ "42": "0xf1ec01d6236d3cd881a0bf0130ea25fe4234003e"
+ }
+ }
+ ]
+ },
+ {
+ "name": "ERC721Proxy",
+ "version": "1.0.0",
+ "changes": [
+ {
+ "note": "protocol v2 deploy",
+ "networks": {
+ "1": "0x208e41fb445f1bb1b6780d58356e81405f3e6127",
+ "3": "0xe654aac058bfbf9f83fcaee7793311dd82f6ddb4",
+ "42": "0x2a9127c745688a165106c11cd4d647d2220af821"
+ }
+ }
+ ]
+ },
+ {
+ "name": "AssetProxyOwner",
+ "version": "1.0.0",
+ "changes": [
+ {
+ "note": "protocol v2 deploy",
+ "networks": {
+ "1": "0x17992e4ffb22730138e4b62aaa6367fa9d3699a6",
+ "3": "0xf5fa5b5fed2727a0e44ac67f6772e97977aa358b",
+ "42": "0x2c824d2882baa668e0d5202b1e7f2922278703f8"
+ }
+ }
+ ]
+ }
+]
diff --git a/contracts/protocol/README.md b/contracts/protocol/README.md
new file mode 100644
index 000000000..5fc9c0a89
--- /dev/null
+++ b/contracts/protocol/README.md
@@ -0,0 +1,74 @@
+## Contracts
+
+Smart contracts that implement the 0x protocol. Addresses of the deployed contracts can be found in the 0x [wiki](https://0xproject.com/wiki#Deployed-Addresses) or the [CHANGELOG](./CHANGELOG.json) of this package.
+
+## Usage
+
+Contracts that make up and interact with version 2.0.0 of the protocol can be found in the [contracts](./contracts) directory. The contents of this directory are broken down into the following subdirectories:
+
+* [protocol](./contracts/protocol)
+ * This directory contains the contracts that make up version 2.0.0. A full specification can be found [here](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
+* [test](./contracts/test)
+ * This directory contains mocks and other contracts that are used solely for testing contracts within the other directories.
+
+## Bug bounty
+
+A bug bounty for the 2.0.0 contracts is ongoing! Instructions can be found [here](https://0xproject.com/wiki#Bug-Bounty).
+
+## Contributing
+
+We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository.
+
+For proposals regarding the 0x protocol's smart contract architecture, message format, or additional functionality, go to the [0x Improvement Proposals (ZEIPs)](https://github.com/0xProject/ZEIPs) repository and follow the contribution guidelines provided therein.
+
+Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started.
+
+### Install Dependencies
+
+If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them:
+
+```bash
+yarn config set workspaces-experimental true
+```
+
+Then install dependencies
+
+```bash
+yarn install
+```
+
+### Build
+
+To build this package and all other monorepo packages that it depends on, run the following from the monorepo root directory:
+
+```bash
+PKG=@0x/contracts-protocol yarn build
+```
+
+Or continuously rebuild on change:
+
+```bash
+PKG=@0x/contracts-protocol yarn watch
+```
+
+### Clean
+
+```bash
+yarn clean
+```
+
+### Lint
+
+```bash
+yarn lint
+```
+
+### Run Tests
+
+```bash
+yarn test
+```
+
+#### Testing options
+
+Contracts testing options like coverage, profiling, revert traces or backing node choosing - are described [here](../TESTING.md).
diff --git a/contracts/protocol/compiler.json b/contracts/protocol/compiler.json
new file mode 100644
index 000000000..10e5bb0a1
--- /dev/null
+++ b/contracts/protocol/compiler.json
@@ -0,0 +1,34 @@
+{
+ "artifactsDir": "./generated-artifacts",
+ "contractsDir": "./contracts",
+ "compilerSettings": {
+ "optimizer": {
+ "enabled": true,
+ "runs": 1000000
+ },
+ "outputSelection": {
+ "*": {
+ "*": [
+ "abi",
+ "evm.bytecode.object",
+ "evm.bytecode.sourceMap",
+ "evm.deployedBytecode.object",
+ "evm.deployedBytecode.sourceMap"
+ ]
+ }
+ }
+ },
+ "contracts": [
+ "AssetProxyOwner",
+ "ERC20Proxy",
+ "ERC721Proxy",
+ "Exchange",
+ "MixinAuthorizable",
+ "MultiAssetProxy",
+ "TestAssetProxyOwner",
+ "TestAssetProxyDispatcher",
+ "TestExchangeInternals",
+ "TestSignatureValidator",
+ "TestStaticCallReceiver"
+ ]
+}
diff --git a/contracts/protocol/contracts/protocol/AssetProxy/ERC20Proxy.sol b/contracts/protocol/contracts/protocol/AssetProxy/ERC20Proxy.sol
new file mode 100644
index 000000000..258443bca
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/AssetProxy/ERC20Proxy.sol
@@ -0,0 +1,184 @@
+/*
+
+ 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;
+
+import "./MixinAuthorizable.sol";
+
+
+contract ERC20Proxy is
+ MixinAuthorizable
+{
+ // Id of this proxy.
+ bytes4 constant internal PROXY_ID = bytes4(keccak256("ERC20Token(address)"));
+
+ // solhint-disable-next-line payable-fallback
+ function ()
+ external
+ {
+ assembly {
+ // The first 4 bytes of calldata holds the function selector
+ let selector := and(calldataload(0), 0xffffffff00000000000000000000000000000000000000000000000000000000)
+
+ // `transferFrom` will be called with the following parameters:
+ // assetData Encoded byte array.
+ // from Address to transfer asset from.
+ // to Address to transfer asset to.
+ // amount Amount of asset to transfer.
+ // bytes4(keccak256("transferFrom(bytes,address,address,uint256)")) = 0xa85e59e4
+ if eq(selector, 0xa85e59e400000000000000000000000000000000000000000000000000000000) {
+
+ // To lookup a value in a mapping, we load from the storage location keccak256(k, p),
+ // where k is the key left padded to 32 bytes and p is the storage slot
+ let start := mload(64)
+ mstore(start, and(caller, 0xffffffffffffffffffffffffffffffffffffffff))
+ mstore(add(start, 32), authorized_slot)
+
+ // Revert if authorized[msg.sender] == false
+ if iszero(sload(keccak256(start, 64))) {
+ // Revert with `Error("SENDER_NOT_AUTHORIZED")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000001553454e4445525f4e4f545f415554484f52495a454400000000000000)
+ mstore(96, 0)
+ revert(0, 100)
+ }
+
+ // `transferFrom`.
+ // The function is marked `external`, so no abi decodeding is done for
+ // us. Instead, we expect the `calldata` memory to contain the
+ // following:
+ //
+ // | Area | Offset | Length | Contents |
+ // |----------|--------|---------|-------------------------------------|
+ // | Header | 0 | 4 | function selector |
+ // | Params | | 4 * 32 | function parameters: |
+ // | | 4 | | 1. offset to assetData (*) |
+ // | | 36 | | 2. from |
+ // | | 68 | | 3. to |
+ // | | 100 | | 4. amount |
+ // | Data | | | assetData: |
+ // | | 132 | 32 | assetData Length |
+ // | | 164 | ** | assetData Contents |
+ //
+ // (*): offset is computed from start of function parameters, so offset
+ // by an additional 4 bytes in the calldata.
+ //
+ // (**): see table below to compute length of assetData Contents
+ //
+ // WARNING: The ABIv2 specification allows additional padding between
+ // the Params and Data section. This will result in a larger
+ // offset to assetData.
+
+ // Asset data itself is encoded as follows:
+ //
+ // | Area | Offset | Length | Contents |
+ // |----------|--------|---------|-------------------------------------|
+ // | Header | 0 | 4 | function selector |
+ // | Params | | 1 * 32 | function parameters: |
+ // | | 4 | 12 + 20 | 1. token address |
+
+ // We construct calldata for the `token.transferFrom` ABI.
+ // The layout of this calldata is in the table below.
+ //
+ // | Area | Offset | Length | Contents |
+ // |----------|--------|---------|-------------------------------------|
+ // | Header | 0 | 4 | function selector |
+ // | Params | | 3 * 32 | function parameters: |
+ // | | 4 | | 1. from |
+ // | | 36 | | 2. to |
+ // | | 68 | | 3. amount |
+
+ /////// Read token address from calldata ///////
+ // * The token address is stored in `assetData`.
+ //
+ // * The "offset to assetData" is stored at offset 4 in the calldata (table 1).
+ // [assetDataOffsetFromParams = calldataload(4)]
+ //
+ // * Notes that the "offset to assetData" is relative to the "Params" area of calldata;
+ // add 4 bytes to account for the length of the "Header" area (table 1).
+ // [assetDataOffsetFromHeader = assetDataOffsetFromParams + 4]
+ //
+ // * The "token address" is offset 32+4=36 bytes into "assetData" (tables 1 & 2).
+ // [tokenOffset = assetDataOffsetFromHeader + 36 = calldataload(4) + 4 + 36]
+ let token := calldataload(add(calldataload(4), 40))
+
+ /////// Setup Header Area ///////
+ // This area holds the 4-byte `transferFrom` selector.
+ // Any trailing data in transferFromSelector will be
+ // overwritten in the next `mstore` call.
+ mstore(0, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
+
+ /////// Setup Params Area ///////
+ // We copy the fields `from`, `to` and `amount` in bulk
+ // from our own calldata to the new calldata.
+ calldatacopy(4, 36, 96)
+
+ /////// Call `token.transferFrom` using the calldata ///////
+ let success := call(
+ gas, // forward all gas
+ token, // call address of token contract
+ 0, // don't send any ETH
+ 0, // pointer to start of input
+ 100, // length of input
+ 0, // write output over input
+ 32 // output size should be 32 bytes
+ )
+
+ /////// Check return data. ///////
+ // If there is no return data, we assume the token incorrectly
+ // does not return a bool. In this case we expect it to revert
+ // on failure, which was handled above.
+ // If the token does return data, we require that it is a single
+ // nonzero 32 bytes value.
+ // So the transfer succeeded if the call succeeded and either
+ // returned nothing, or returned a non-zero 32 byte value.
+ success := and(success, or(
+ iszero(returndatasize),
+ and(
+ eq(returndatasize, 32),
+ gt(mload(0), 0)
+ )
+ ))
+ if success {
+ return(0, 0)
+ }
+
+ // Revert with `Error("TRANSFER_FAILED")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000000f5452414e534645525f4641494c454400000000000000000000000000)
+ mstore(96, 0)
+ revert(0, 100)
+ }
+
+ // Revert if undefined function is called
+ revert(0, 0)
+ }
+ }
+
+ /// @dev Gets the proxy id associated with the proxy address.
+ /// @return Proxy id.
+ function getProxyId()
+ external
+ pure
+ returns (bytes4)
+ {
+ return PROXY_ID;
+ }
+}
diff --git a/contracts/protocol/contracts/protocol/AssetProxy/ERC721Proxy.sol b/contracts/protocol/contracts/protocol/AssetProxy/ERC721Proxy.sol
new file mode 100644
index 000000000..65b664b8b
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/AssetProxy/ERC721Proxy.sol
@@ -0,0 +1,171 @@
+/*
+
+ 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;
+
+import "./MixinAuthorizable.sol";
+
+
+contract ERC721Proxy is
+ MixinAuthorizable
+{
+ // Id of this proxy.
+ bytes4 constant internal PROXY_ID = bytes4(keccak256("ERC721Token(address,uint256)"));
+
+ // solhint-disable-next-line payable-fallback
+ function ()
+ external
+ {
+ assembly {
+ // The first 4 bytes of calldata holds the function selector
+ let selector := and(calldataload(0), 0xffffffff00000000000000000000000000000000000000000000000000000000)
+
+ // `transferFrom` will be called with the following parameters:
+ // assetData Encoded byte array.
+ // from Address to transfer asset from.
+ // to Address to transfer asset to.
+ // amount Amount of asset to transfer.
+ // bytes4(keccak256("transferFrom(bytes,address,address,uint256)")) = 0xa85e59e4
+ if eq(selector, 0xa85e59e400000000000000000000000000000000000000000000000000000000) {
+
+ // To lookup a value in a mapping, we load from the storage location keccak256(k, p),
+ // where k is the key left padded to 32 bytes and p is the storage slot
+ let start := mload(64)
+ mstore(start, and(caller, 0xffffffffffffffffffffffffffffffffffffffff))
+ mstore(add(start, 32), authorized_slot)
+
+ // Revert if authorized[msg.sender] == false
+ if iszero(sload(keccak256(start, 64))) {
+ // Revert with `Error("SENDER_NOT_AUTHORIZED")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000001553454e4445525f4e4f545f415554484f52495a454400000000000000)
+ mstore(96, 0)
+ revert(0, 100)
+ }
+
+ // `transferFrom`.
+ // The function is marked `external`, so no abi decodeding is done for
+ // us. Instead, we expect the `calldata` memory to contain the
+ // following:
+ //
+ // | Area | Offset | Length | Contents |
+ // |----------|--------|---------|-------------------------------------|
+ // | Header | 0 | 4 | function selector |
+ // | Params | | 4 * 32 | function parameters: |
+ // | | 4 | | 1. offset to assetData (*) |
+ // | | 36 | | 2. from |
+ // | | 68 | | 3. to |
+ // | | 100 | | 4. amount |
+ // | Data | | | assetData: |
+ // | | 132 | 32 | assetData Length |
+ // | | 164 | ** | assetData Contents |
+ //
+ // (*): offset is computed from start of function parameters, so offset
+ // by an additional 4 bytes in the calldata.
+ //
+ // (**): see table below to compute length of assetData Contents
+ //
+ // WARNING: The ABIv2 specification allows additional padding between
+ // the Params and Data section. This will result in a larger
+ // offset to assetData.
+
+ // Asset data itself is encoded as follows:
+ //
+ // | Area | Offset | Length | Contents |
+ // |----------|--------|---------|-------------------------------------|
+ // | Header | 0 | 4 | function selector |
+ // | Params | | 2 * 32 | function parameters: |
+ // | | 4 | 12 + 20 | 1. token address |
+ // | | 36 | | 2. tokenId |
+
+ // We construct calldata for the `token.transferFrom` ABI.
+ // The layout of this calldata is in the table below.
+ //
+ // | Area | Offset | Length | Contents |
+ // |----------|--------|---------|-------------------------------------|
+ // | Header | 0 | 4 | function selector |
+ // | Params | | 3 * 32 | function parameters: |
+ // | | 4 | | 1. from |
+ // | | 36 | | 2. to |
+ // | | 68 | | 3. tokenId |
+
+ // There exists only 1 of each token.
+ // require(amount == 1, "INVALID_AMOUNT")
+ if sub(calldataload(100), 1) {
+ // Revert with `Error("INVALID_AMOUNT")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000000e494e56414c49445f414d4f554e540000000000000000000000000000)
+ mstore(96, 0)
+ revert(0, 100)
+ }
+
+ /////// Setup Header Area ///////
+ // This area holds the 4-byte `transferFrom` selector.
+ // Any trailing data in transferFromSelector will be
+ // overwritten in the next `mstore` call.
+ mstore(0, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
+
+ /////// Setup Params Area ///////
+ // We copy the fields `from` and `to` in bulk
+ // from our own calldata to the new calldata.
+ calldatacopy(4, 36, 64)
+
+ // Copy `tokenId` field from our own calldata to the new calldata.
+ let assetDataOffset := calldataload(4)
+ calldatacopy(68, add(assetDataOffset, 72), 32)
+
+ /////// Call `token.transferFrom` using the calldata ///////
+ let token := calldataload(add(assetDataOffset, 40))
+ let success := call(
+ gas, // forward all gas
+ token, // call address of token contract
+ 0, // don't send any ETH
+ 0, // pointer to start of input
+ 100, // length of input
+ 0, // write output to null
+ 0 // output size is 0 bytes
+ )
+ if success {
+ return(0, 0)
+ }
+
+ // Revert with `Error("TRANSFER_FAILED")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000000f5452414e534645525f4641494c454400000000000000000000000000)
+ mstore(96, 0)
+ revert(0, 100)
+ }
+
+ // Revert if undefined function is called
+ revert(0, 0)
+ }
+ }
+
+ /// @dev Gets the proxy id associated with the proxy address.
+ /// @return Proxy id.
+ function getProxyId()
+ external
+ pure
+ returns (bytes4)
+ {
+ return PROXY_ID;
+ }
+}
diff --git a/contracts/protocol/contracts/protocol/AssetProxy/MixinAuthorizable.sol b/contracts/protocol/contracts/protocol/AssetProxy/MixinAuthorizable.sol
new file mode 100644
index 000000000..08f9b94dc
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/AssetProxy/MixinAuthorizable.sol
@@ -0,0 +1,117 @@
+/*
+
+ 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;
+
+import "@0x/contracts-utils/contracts/utils/Ownable/Ownable.sol";
+import "./mixins/MAuthorizable.sol";
+
+
+contract MixinAuthorizable is
+ Ownable,
+ MAuthorizable
+{
+ /// @dev Only authorized addresses can invoke functions with this modifier.
+ modifier onlyAuthorized {
+ require(
+ authorized[msg.sender],
+ "SENDER_NOT_AUTHORIZED"
+ );
+ _;
+ }
+
+ mapping (address => bool) public authorized;
+ address[] public authorities;
+
+ /// @dev Authorizes an address.
+ /// @param target Address to authorize.
+ function addAuthorizedAddress(address target)
+ external
+ onlyOwner
+ {
+ require(
+ !authorized[target],
+ "TARGET_ALREADY_AUTHORIZED"
+ );
+
+ authorized[target] = true;
+ authorities.push(target);
+ emit AuthorizedAddressAdded(target, msg.sender);
+ }
+
+ /// @dev Removes authorizion of an address.
+ /// @param target Address to remove authorization from.
+ function removeAuthorizedAddress(address target)
+ external
+ onlyOwner
+ {
+ require(
+ authorized[target],
+ "TARGET_NOT_AUTHORIZED"
+ );
+
+ delete authorized[target];
+ for (uint256 i = 0; i < authorities.length; i++) {
+ if (authorities[i] == target) {
+ authorities[i] = authorities[authorities.length - 1];
+ authorities.length -= 1;
+ break;
+ }
+ }
+ emit AuthorizedAddressRemoved(target, msg.sender);
+ }
+
+ /// @dev Removes authorizion of an address.
+ /// @param target Address to remove authorization from.
+ /// @param index Index of target in authorities array.
+ function removeAuthorizedAddressAtIndex(
+ address target,
+ uint256 index
+ )
+ external
+ onlyOwner
+ {
+ require(
+ authorized[target],
+ "TARGET_NOT_AUTHORIZED"
+ );
+ require(
+ index < authorities.length,
+ "INDEX_OUT_OF_BOUNDS"
+ );
+ require(
+ authorities[index] == target,
+ "AUTHORIZED_ADDRESS_MISMATCH"
+ );
+
+ delete authorized[target];
+ authorities[index] = authorities[authorities.length - 1];
+ authorities.length -= 1;
+ emit AuthorizedAddressRemoved(target, msg.sender);
+ }
+
+ /// @dev Gets all authorized addresses.
+ /// @return Array of authorized addresses.
+ function getAuthorizedAddresses()
+ external
+ view
+ returns (address[] memory)
+ {
+ return authorities;
+ }
+}
diff --git a/contracts/protocol/contracts/protocol/AssetProxy/MultiAssetProxy.sol b/contracts/protocol/contracts/protocol/AssetProxy/MultiAssetProxy.sol
new file mode 100644
index 000000000..42231e73b
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/AssetProxy/MultiAssetProxy.sol
@@ -0,0 +1,300 @@
+/*
+
+ 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;
+
+import "../Exchange/MixinAssetProxyDispatcher.sol";
+import "./MixinAuthorizable.sol";
+
+
+contract MultiAssetProxy is
+ MixinAssetProxyDispatcher,
+ MixinAuthorizable
+{
+ // Id of this proxy.
+ bytes4 constant internal PROXY_ID = bytes4(keccak256("MultiAsset(uint256[],bytes[])"));
+
+ // solhint-disable-next-line payable-fallback
+ function ()
+ external
+ {
+ assembly {
+ // The first 4 bytes of calldata holds the function selector
+ let selector := and(calldataload(0), 0xffffffff00000000000000000000000000000000000000000000000000000000)
+
+ // `transferFrom` will be called with the following parameters:
+ // assetData Encoded byte array.
+ // from Address to transfer asset from.
+ // to Address to transfer asset to.
+ // amount Amount of asset to transfer.
+ // bytes4(keccak256("transferFrom(bytes,address,address,uint256)")) = 0xa85e59e4
+ if eq(selector, 0xa85e59e400000000000000000000000000000000000000000000000000000000) {
+
+ // To lookup a value in a mapping, we load from the storage location keccak256(k, p),
+ // where k is the key left padded to 32 bytes and p is the storage slot
+ mstore(0, caller)
+ mstore(32, authorized_slot)
+
+ // Revert if authorized[msg.sender] == false
+ if iszero(sload(keccak256(0, 64))) {
+ // Revert with `Error("SENDER_NOT_AUTHORIZED")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000001553454e4445525f4e4f545f415554484f52495a454400000000000000)
+ mstore(96, 0)
+ revert(0, 100)
+ }
+
+ // `transferFrom`.
+ // The function is marked `external`, so no abi decoding is done for
+ // us. Instead, we expect the `calldata` memory to contain the
+ // following:
+ //
+ // | Area | Offset | Length | Contents |
+ // |----------|--------|---------|-------------------------------------|
+ // | Header | 0 | 4 | function selector |
+ // | Params | | 4 * 32 | function parameters: |
+ // | | 4 | | 1. offset to assetData (*) |
+ // | | 36 | | 2. from |
+ // | | 68 | | 3. to |
+ // | | 100 | | 4. amount |
+ // | Data | | | assetData: |
+ // | | 132 | 32 | assetData Length |
+ // | | 164 | ** | assetData Contents |
+ //
+ // (*): offset is computed from start of function parameters, so offset
+ // by an additional 4 bytes in the calldata.
+ //
+ // (**): see table below to compute length of assetData Contents
+ //
+ // WARNING: The ABIv2 specification allows additional padding between
+ // the Params and Data section. This will result in a larger
+ // offset to assetData.
+
+ // Load offset to `assetData`
+ let assetDataOffset := calldataload(4)
+
+ // Asset data itself is encoded as follows:
+ //
+ // | Area | Offset | Length | Contents |
+ // |----------|-------------|---------|-------------------------------------|
+ // | Header | 0 | 4 | assetProxyId |
+ // | Params | | 2 * 32 | function parameters: |
+ // | | 4 | | 1. offset to amounts (*) |
+ // | | 36 | | 2. offset to nestedAssetData (*) |
+ // | Data | | | amounts: |
+ // | | 68 | 32 | amounts Length |
+ // | | 100 | a | amounts Contents |
+ // | | | | nestedAssetData: |
+ // | | 100 + a | 32 | nestedAssetData Length |
+ // | | 132 + a | b | nestedAssetData Contents (offsets) |
+ // | | 132 + a + b | | nestedAssetData[0, ..., len] |
+
+ // In order to find the offset to `amounts`, we must add:
+ // 4 (function selector)
+ // + assetDataOffset
+ // + 32 (assetData len)
+ // + 4 (assetProxyId)
+ let amountsOffset := calldataload(add(assetDataOffset, 40))
+
+ // In order to find the offset to `nestedAssetData`, we must add:
+ // 4 (function selector)
+ // + assetDataOffset
+ // + 32 (assetData len)
+ // + 4 (assetProxyId)
+ // + 32 (amounts offset)
+ let nestedAssetDataOffset := calldataload(add(assetDataOffset, 72))
+
+ // In order to find the start of the `amounts` contents, we must add:
+ // 4 (function selector)
+ // + assetDataOffset
+ // + 32 (assetData len)
+ // + 4 (assetProxyId)
+ // + amountsOffset
+ // + 32 (amounts len)
+ let amountsContentsStart := add(assetDataOffset, add(amountsOffset, 72))
+
+ // Load number of elements in `amounts`
+ let amountsLen := calldataload(sub(amountsContentsStart, 32))
+
+ // In order to find the start of the `nestedAssetData` contents, we must add:
+ // 4 (function selector)
+ // + assetDataOffset
+ // + 32 (assetData len)
+ // + 4 (assetProxyId)
+ // + nestedAssetDataOffset
+ // + 32 (nestedAssetData len)
+ let nestedAssetDataContentsStart := add(assetDataOffset, add(nestedAssetDataOffset, 72))
+
+ // Load number of elements in `nestedAssetData`
+ let nestedAssetDataLen := calldataload(sub(nestedAssetDataContentsStart, 32))
+
+ // Revert if number of elements in `amounts` differs from number of elements in `nestedAssetData`
+ if iszero(eq(amountsLen, nestedAssetDataLen)) {
+ // Revert with `Error("LENGTH_MISMATCH")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000000f4c454e4754485f4d49534d4154434800000000000000000000000000)
+ mstore(96, 0)
+ revert(0, 100)
+ }
+
+ // Copy `transferFrom` selector, offset to `assetData`, `from`, and `to` from calldata to memory
+ calldatacopy(
+ 0, // memory can safely be overwritten from beginning
+ 0, // start of calldata
+ 100 // length of selector (4) and 3 params (32 * 3)
+ )
+
+ // Overwrite existing offset to `assetData` with our own
+ mstore(4, 128)
+
+ // Load `amount`
+ let amount := calldataload(100)
+
+ // Calculate number of bytes in `amounts` contents
+ let amountsByteLen := mul(amountsLen, 32)
+
+ // Initialize `assetProxyId` and `assetProxy` to 0
+ let assetProxyId := 0
+ let assetProxy := 0
+
+ // Loop through `amounts` and `nestedAssetData`, calling `transferFrom` for each respective element
+ for {let i := 0} lt(i, amountsByteLen) {i := add(i, 32)} {
+
+ // Calculate the total amount
+ let amountsElement := calldataload(add(amountsContentsStart, i))
+ let totalAmount := mul(amountsElement, amount)
+
+ // Revert if multiplication resulted in an overflow
+ if iszero(eq(div(totalAmount, amount), amountsElement)) {
+ // Revert with `Error("UINT256_OVERFLOW")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000001055494e543235365f4f564552464c4f57000000000000000000000000)
+ mstore(96, 0)
+ revert(0, 100)
+ }
+
+ // Write `totalAmount` to memory
+ mstore(100, totalAmount)
+
+ // Load offset to `nestedAssetData[i]`
+ let nestedAssetDataElementOffset := calldataload(add(nestedAssetDataContentsStart, i))
+
+ // In order to find the start of the `nestedAssetData[i]` contents, we must add:
+ // 4 (function selector)
+ // + assetDataOffset
+ // + 32 (assetData len)
+ // + 4 (assetProxyId)
+ // + nestedAssetDataOffset
+ // + 32 (nestedAssetData len)
+ // + nestedAssetDataElementOffset
+ // + 32 (nestedAssetDataElement len)
+ let nestedAssetDataElementContentsStart := add(assetDataOffset, add(nestedAssetDataOffset, add(nestedAssetDataElementOffset, 104)))
+
+ // Load length of `nestedAssetData[i]`
+ let nestedAssetDataElementLenStart := sub(nestedAssetDataElementContentsStart, 32)
+ let nestedAssetDataElementLen := calldataload(nestedAssetDataElementLenStart)
+
+ // Revert if the `nestedAssetData` does not contain a 4 byte `assetProxyId`
+ if lt(nestedAssetDataElementLen, 4) {
+ // Revert with `Error("LENGTH_GREATER_THAN_3_REQUIRED")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000001e4c454e4754485f475245415445525f5448414e5f335f524551554952)
+ mstore(96, 0x4544000000000000000000000000000000000000000000000000000000000000)
+ revert(0, 100)
+ }
+
+ // Load AssetProxy id
+ let currentAssetProxyId := and(
+ calldataload(nestedAssetDataElementContentsStart),
+ 0xffffffff00000000000000000000000000000000000000000000000000000000
+ )
+
+ // Only load `assetProxy` if `currentAssetProxyId` does not equal `assetProxyId`
+ // We do not need to check if `currentAssetProxyId` is 0 since `assetProxy` is also initialized to 0
+ if iszero(eq(currentAssetProxyId, assetProxyId)) {
+ // Update `assetProxyId`
+ assetProxyId := currentAssetProxyId
+ // To lookup a value in a mapping, we load from the storage location keccak256(k, p),
+ // where k is the key left padded to 32 bytes and p is the storage slot
+ mstore(132, assetProxyId)
+ mstore(164, assetProxies_slot)
+ assetProxy := sload(keccak256(132, 64))
+ }
+
+ // Revert if AssetProxy with given id does not exist
+ if iszero(assetProxy) {
+ // Revert with `Error("ASSET_PROXY_DOES_NOT_EXIST")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000001a41535345545f50524f58595f444f45535f4e4f545f45584953540000)
+ mstore(96, 0)
+ revert(0, 100)
+ }
+
+ // Copy `nestedAssetData[i]` from calldata to memory
+ calldatacopy(
+ 132, // memory slot after `amounts[i]`
+ nestedAssetDataElementLenStart, // location of `nestedAssetData[i]` in calldata
+ add(nestedAssetDataElementLen, 32) // `nestedAssetData[i].length` plus 32 byte length
+ )
+
+ // call `assetProxy.transferFrom`
+ let success := call(
+ gas, // forward all gas
+ assetProxy, // call address of asset proxy
+ 0, // don't send any ETH
+ 0, // pointer to start of input
+ add(164, nestedAssetDataElementLen), // length of input
+ 0, // write output over memory that won't be reused
+ 0 // don't copy output to memory
+ )
+
+ // Revert with reason given by AssetProxy if `transferFrom` call failed
+ if iszero(success) {
+ returndatacopy(
+ 0, // copy to memory at 0
+ 0, // copy from return data at 0
+ returndatasize() // copy all return data
+ )
+ revert(0, returndatasize())
+ }
+ }
+
+ // Return if no `transferFrom` calls reverted
+ return(0, 0)
+ }
+
+ // Revert if undefined function is called
+ revert(0, 0)
+ }
+ }
+
+ /// @dev Gets the proxy id associated with the proxy address.
+ /// @return Proxy id.
+ function getProxyId()
+ external
+ pure
+ returns (bytes4)
+ {
+ return PROXY_ID;
+ }
+}
diff --git a/contracts/protocol/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol b/contracts/protocol/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol
new file mode 100644
index 000000000..fe77048ce
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol
@@ -0,0 +1,41 @@
+/*
+
+ 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;
+
+import "@0x/contracts-interfaces/contracts/protocol/AssetProxy/IAuthorizable.sol";
+
+
+contract MAuthorizable is
+ IAuthorizable
+{
+ // Event logged when a new address is authorized.
+ event AuthorizedAddressAdded(
+ address indexed target,
+ address indexed caller
+ );
+
+ // Event logged when a currently authorized address is unauthorized.
+ event AuthorizedAddressRemoved(
+ address indexed target,
+ address indexed caller
+ );
+
+ /// @dev Only authorized addresses can invoke functions with this modifier.
+ modifier onlyAuthorized { revert(); _; }
+}
diff --git a/contracts/protocol/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol b/contracts/protocol/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol
new file mode 100644
index 000000000..bfc7b5a66
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol
@@ -0,0 +1,108 @@
+/*
+
+ 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;
+
+import "@0x/contracts-multisig/contracts/multisig/MultiSigWalletWithTimeLock.sol";
+import "@0x/contracts-utils/contracts/utils/LibBytes/LibBytes.sol";
+
+
+contract AssetProxyOwner is
+ MultiSigWalletWithTimeLock
+{
+ using LibBytes for bytes;
+
+ event AssetProxyRegistration(address assetProxyContract, bool isRegistered);
+
+ // Mapping of AssetProxy contract address =>
+ // if this contract is allowed to call the AssetProxy's `removeAuthorizedAddressAtIndex` method without a time lock.
+ mapping (address => bool) public isAssetProxyRegistered;
+
+ bytes4 constant internal REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR = bytes4(keccak256("removeAuthorizedAddressAtIndex(address,uint256)"));
+
+ /// @dev Function will revert if the transaction does not call `removeAuthorizedAddressAtIndex`
+ /// on an approved AssetProxy contract.
+ modifier validRemoveAuthorizedAddressAtIndexTx(uint256 transactionId) {
+ Transaction storage txn = transactions[transactionId];
+ require(
+ isAssetProxyRegistered[txn.destination],
+ "UNREGISTERED_ASSET_PROXY"
+ );
+ require(
+ txn.data.readBytes4(0) == REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR,
+ "INVALID_FUNCTION_SELECTOR"
+ );
+ _;
+ }
+
+ /// @dev Contract constructor sets initial owners, required number of confirmations,
+ /// time lock, and list of AssetProxy addresses.
+ /// @param _owners List of initial owners.
+ /// @param _assetProxyContracts Array of AssetProxy contract addresses.
+ /// @param _required Number of required confirmations.
+ /// @param _secondsTimeLocked Duration needed after a transaction is confirmed and before it becomes executable, in seconds.
+ constructor (
+ address[] memory _owners,
+ address[] memory _assetProxyContracts,
+ uint256 _required,
+ uint256 _secondsTimeLocked
+ )
+ public
+ MultiSigWalletWithTimeLock(_owners, _required, _secondsTimeLocked)
+ {
+ for (uint256 i = 0; i < _assetProxyContracts.length; i++) {
+ address assetProxy = _assetProxyContracts[i];
+ require(
+ assetProxy != address(0),
+ "INVALID_ASSET_PROXY"
+ );
+ isAssetProxyRegistered[assetProxy] = true;
+ }
+ }
+
+ /// @dev Registers or deregisters an AssetProxy to be able to execute
+ /// `removeAuthorizedAddressAtIndex` without a timelock.
+ /// @param assetProxyContract Address of AssetProxy contract.
+ /// @param isRegistered Status of approval for AssetProxy contract.
+ function registerAssetProxy(address assetProxyContract, bool isRegistered)
+ public
+ onlyWallet
+ notNull(assetProxyContract)
+ {
+ isAssetProxyRegistered[assetProxyContract] = isRegistered;
+ emit AssetProxyRegistration(assetProxyContract, isRegistered);
+ }
+
+ /// @dev Allows execution of `removeAuthorizedAddressAtIndex` without time lock.
+ /// @param transactionId Transaction ID.
+ function executeRemoveAuthorizedAddressAtIndex(uint256 transactionId)
+ public
+ notExecuted(transactionId)
+ fullyConfirmed(transactionId)
+ validRemoveAuthorizedAddressAtIndexTx(transactionId)
+ {
+ Transaction storage txn = transactions[transactionId];
+ txn.executed = true;
+ if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) {
+ emit Execution(transactionId);
+ } else {
+ emit ExecutionFailure(transactionId);
+ txn.executed = false;
+ }
+ }
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/Exchange.sol b/contracts/protocol/contracts/protocol/Exchange/Exchange.sol
new file mode 100644
index 000000000..65ca742ea
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/Exchange.sol
@@ -0,0 +1,53 @@
+/*
+
+ 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;
+pragma experimental ABIEncoderV2;
+
+import "@0x/contracts-libs/contracts/libs/LibConstants.sol";
+import "./MixinExchangeCore.sol";
+import "./MixinSignatureValidator.sol";
+import "./MixinWrapperFunctions.sol";
+import "./MixinAssetProxyDispatcher.sol";
+import "./MixinTransactions.sol";
+import "./MixinMatchOrders.sol";
+
+
+// solhint-disable no-empty-blocks
+contract Exchange is
+ MixinExchangeCore,
+ MixinMatchOrders,
+ MixinSignatureValidator,
+ MixinTransactions,
+ MixinAssetProxyDispatcher,
+ MixinWrapperFunctions
+{
+ string constant public VERSION = "2.0.1-alpha";
+
+ // Mixins are instantiated in the order they are inherited
+ constructor (bytes memory _zrxAssetData)
+ public
+ LibConstants(_zrxAssetData) // @TODO: Remove when we deploy.
+ MixinExchangeCore()
+ MixinMatchOrders()
+ MixinSignatureValidator()
+ MixinTransactions()
+ MixinAssetProxyDispatcher()
+ MixinWrapperFunctions()
+ {}
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol b/contracts/protocol/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol
new file mode 100644
index 000000000..36ab39b45
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol
@@ -0,0 +1,174 @@
+/*
+
+ 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;
+
+import "@0x/contracts-utils/contracts/utils/Ownable/Ownable.sol";
+import "./mixins/MAssetProxyDispatcher.sol";
+import "@0x/contracts-interfaces/contracts/protocol/AssetProxy/IAssetProxy.sol";
+
+
+contract MixinAssetProxyDispatcher is
+ Ownable,
+ MAssetProxyDispatcher
+{
+ // Mapping from Asset Proxy Id's to their respective Asset Proxy
+ mapping (bytes4 => IAssetProxy) public assetProxies;
+
+ /// @dev Registers an asset proxy to its asset proxy id.
+ /// Once an asset proxy is registered, it cannot be unregistered.
+ /// @param assetProxy Address of new asset proxy to register.
+ function registerAssetProxy(address assetProxy)
+ external
+ onlyOwner
+ {
+ IAssetProxy assetProxyContract = IAssetProxy(assetProxy);
+
+ // Ensure that no asset proxy exists with current id.
+ bytes4 assetProxyId = assetProxyContract.getProxyId();
+ address currentAssetProxy = assetProxies[assetProxyId];
+ require(
+ currentAssetProxy == address(0),
+ "ASSET_PROXY_ALREADY_EXISTS"
+ );
+
+ // Add asset proxy and log registration.
+ assetProxies[assetProxyId] = assetProxyContract;
+ emit AssetProxyRegistered(
+ assetProxyId,
+ assetProxy
+ );
+ }
+
+ /// @dev Gets an asset proxy.
+ /// @param assetProxyId Id of the asset proxy.
+ /// @return The asset proxy registered to assetProxyId. Returns 0x0 if no proxy is registered.
+ function getAssetProxy(bytes4 assetProxyId)
+ external
+ view
+ returns (address)
+ {
+ return assetProxies[assetProxyId];
+ }
+
+ /// @dev Forwards arguments to assetProxy and calls `transferFrom`. Either succeeds or throws.
+ /// @param assetData Byte array encoded for the asset.
+ /// @param from Address to transfer token from.
+ /// @param to Address to transfer token to.
+ /// @param amount Amount of token to transfer.
+ function dispatchTransferFrom(
+ bytes memory assetData,
+ address from,
+ address to,
+ uint256 amount
+ )
+ internal
+ {
+ // Do nothing if no amount should be transferred.
+ if (amount > 0 && from != to) {
+ // Ensure assetData length is valid
+ require(
+ assetData.length > 3,
+ "LENGTH_GREATER_THAN_3_REQUIRED"
+ );
+
+ // Lookup assetProxy. We do not use `LibBytes.readBytes4` for gas efficiency reasons.
+ bytes4 assetProxyId;
+ assembly {
+ assetProxyId := and(mload(
+ add(assetData, 32)),
+ 0xFFFFFFFF00000000000000000000000000000000000000000000000000000000
+ )
+ }
+ address assetProxy = assetProxies[assetProxyId];
+
+ // Ensure that assetProxy exists
+ require(
+ assetProxy != address(0),
+ "ASSET_PROXY_DOES_NOT_EXIST"
+ );
+
+ // We construct calldata for the `assetProxy.transferFrom` ABI.
+ // The layout of this calldata is in the table below.
+ //
+ // | Area | Offset | Length | Contents |
+ // | -------- |--------|---------|-------------------------------------------- |
+ // | Header | 0 | 4 | function selector |
+ // | Params | | 4 * 32 | function parameters: |
+ // | | 4 | | 1. offset to assetData (*) |
+ // | | 36 | | 2. from |
+ // | | 68 | | 3. to |
+ // | | 100 | | 4. amount |
+ // | Data | | | assetData: |
+ // | | 132 | 32 | assetData Length |
+ // | | 164 | ** | assetData Contents |
+
+ assembly {
+ /////// Setup State ///////
+ // `cdStart` is the start of the calldata for `assetProxy.transferFrom` (equal to free memory ptr).
+ let cdStart := mload(64)
+ // `dataAreaLength` is the total number of words needed to store `assetData`
+ // As-per the ABI spec, this value is padded up to the nearest multiple of 32,
+ // and includes 32-bytes for length.
+ let dataAreaLength := and(add(mload(assetData), 63), 0xFFFFFFFFFFFE0)
+ // `cdEnd` is the end of the calldata for `assetProxy.transferFrom`.
+ let cdEnd := add(cdStart, add(132, dataAreaLength))
+
+
+ /////// Setup Header Area ///////
+ // This area holds the 4-byte `transferFromSelector`.
+ // bytes4(keccak256("transferFrom(bytes,address,address,uint256)")) = 0xa85e59e4
+ mstore(cdStart, 0xa85e59e400000000000000000000000000000000000000000000000000000000)
+
+ /////// Setup Params Area ///////
+ // Each parameter is padded to 32-bytes. The entire Params Area is 128 bytes.
+ // Notes:
+ // 1. The offset to `assetData` is the length of the Params Area (128 bytes).
+ // 2. A 20-byte mask is applied to addresses to zero-out the unused bytes.
+ mstore(add(cdStart, 4), 128)
+ mstore(add(cdStart, 36), and(from, 0xffffffffffffffffffffffffffffffffffffffff))
+ mstore(add(cdStart, 68), and(to, 0xffffffffffffffffffffffffffffffffffffffff))
+ mstore(add(cdStart, 100), amount)
+
+ /////// Setup Data Area ///////
+ // This area holds `assetData`.
+ let dataArea := add(cdStart, 132)
+ // solhint-disable-next-line no-empty-blocks
+ for {} lt(dataArea, cdEnd) {} {
+ mstore(dataArea, mload(assetData))
+ dataArea := add(dataArea, 32)
+ assetData := add(assetData, 32)
+ }
+
+ /////// Call `assetProxy.transferFrom` using the constructed calldata ///////
+ let success := call(
+ gas, // forward all gas
+ assetProxy, // call address of asset proxy
+ 0, // don't send any ETH
+ cdStart, // pointer to start of input
+ sub(cdEnd, cdStart), // length of input
+ cdStart, // write output over input
+ 512 // reserve 512 bytes for output
+ )
+ if iszero(success) {
+ revert(cdStart, returndatasize())
+ }
+ }
+ }
+ }
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/MixinExchangeCore.sol b/contracts/protocol/contracts/protocol/Exchange/MixinExchangeCore.sol
new file mode 100644
index 000000000..68d6a3897
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/MixinExchangeCore.sol
@@ -0,0 +1,529 @@
+/*
+
+ 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;
+pragma experimental ABIEncoderV2;
+
+import "@0x/contracts-utils/contracts/utils/ReentrancyGuard/ReentrancyGuard.sol";
+import "@0x/contracts-libs/contracts/libs/LibConstants.sol";
+import "@0x/contracts-libs/contracts/libs/LibFillResults.sol";
+import "@0x/contracts-libs/contracts/libs/LibOrder.sol";
+import "@0x/contracts-libs/contracts/libs/LibMath.sol";
+import "./mixins/MExchangeCore.sol";
+import "./mixins/MSignatureValidator.sol";
+import "./mixins/MTransactions.sol";
+import "./mixins/MAssetProxyDispatcher.sol";
+
+
+contract MixinExchangeCore is
+ ReentrancyGuard,
+ LibConstants,
+ LibMath,
+ LibOrder,
+ LibFillResults,
+ MAssetProxyDispatcher,
+ MExchangeCore,
+ MSignatureValidator,
+ MTransactions
+{
+ // Mapping of orderHash => amount of takerAsset already bought by maker
+ mapping (bytes32 => uint256) public filled;
+
+ // Mapping of orderHash => cancelled
+ mapping (bytes32 => bool) public cancelled;
+
+ // Mapping of makerAddress => senderAddress => lowest salt an order can have in order to be fillable
+ // Orders with specified senderAddress and with a salt less than their epoch are considered cancelled
+ mapping (address => mapping (address => uint256)) public orderEpoch;
+
+ /// @dev Cancels all orders created by makerAddress with a salt less than or equal to the targetOrderEpoch
+ /// and senderAddress equal to msg.sender (or null address if msg.sender == makerAddress).
+ /// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
+ function cancelOrdersUpTo(uint256 targetOrderEpoch)
+ external
+ nonReentrant
+ {
+ address makerAddress = getCurrentContextAddress();
+ // If this function is called via `executeTransaction`, we only update the orderEpoch for the makerAddress/msg.sender combination.
+ // This allows external filter contracts to add rules to how orders are cancelled via this function.
+ address senderAddress = makerAddress == msg.sender ? address(0) : msg.sender;
+
+ // orderEpoch is initialized to 0, so to cancelUpTo we need salt + 1
+ uint256 newOrderEpoch = targetOrderEpoch + 1;
+ uint256 oldOrderEpoch = orderEpoch[makerAddress][senderAddress];
+
+ // Ensure orderEpoch is monotonically increasing
+ require(
+ newOrderEpoch > oldOrderEpoch,
+ "INVALID_NEW_ORDER_EPOCH"
+ );
+
+ // Update orderEpoch
+ orderEpoch[makerAddress][senderAddress] = newOrderEpoch;
+ emit CancelUpTo(
+ makerAddress,
+ senderAddress,
+ newOrderEpoch
+ );
+ }
+
+ /// @dev Fills the input order.
+ /// @param order Order struct containing order specifications.
+ /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
+ /// @param signature Proof that order has been created by maker.
+ /// @return Amounts filled and fees paid by maker and taker.
+ function fillOrder(
+ Order memory order,
+ uint256 takerAssetFillAmount,
+ bytes memory signature
+ )
+ public
+ nonReentrant
+ returns (FillResults memory fillResults)
+ {
+ fillResults = fillOrderInternal(
+ order,
+ takerAssetFillAmount,
+ signature
+ );
+ return fillResults;
+ }
+
+ /// @dev After calling, the order can not be filled anymore.
+ /// Throws if order is invalid or sender does not have permission to cancel.
+ /// @param order Order to cancel. Order must be OrderStatus.FILLABLE.
+ function cancelOrder(Order memory order)
+ public
+ nonReentrant
+ {
+ cancelOrderInternal(order);
+ }
+
+ /// @dev Gets information about an order: status, hash, and amount filled.
+ /// @param order Order to gather information on.
+ /// @return OrderInfo Information about the order and its state.
+ /// See LibOrder.OrderInfo for a complete description.
+ function getOrderInfo(Order memory order)
+ public
+ view
+ returns (OrderInfo memory orderInfo)
+ {
+ // Compute the order hash
+ orderInfo.orderHash = getOrderHash(order);
+
+ // Fetch filled amount
+ orderInfo.orderTakerAssetFilledAmount = filled[orderInfo.orderHash];
+
+ // If order.makerAssetAmount is zero, we also reject the order.
+ // While the Exchange contract handles them correctly, they create
+ // edge cases in the supporting infrastructure because they have
+ // an 'infinite' price when computed by a simple division.
+ if (order.makerAssetAmount == 0) {
+ orderInfo.orderStatus = uint8(OrderStatus.INVALID_MAKER_ASSET_AMOUNT);
+ return orderInfo;
+ }
+
+ // If order.takerAssetAmount is zero, then the order will always
+ // be considered filled because 0 == takerAssetAmount == orderTakerAssetFilledAmount
+ // Instead of distinguishing between unfilled and filled zero taker
+ // amount orders, we choose not to support them.
+ if (order.takerAssetAmount == 0) {
+ orderInfo.orderStatus = uint8(OrderStatus.INVALID_TAKER_ASSET_AMOUNT);
+ return orderInfo;
+ }
+
+ // Validate order availability
+ if (orderInfo.orderTakerAssetFilledAmount >= order.takerAssetAmount) {
+ orderInfo.orderStatus = uint8(OrderStatus.FULLY_FILLED);
+ return orderInfo;
+ }
+
+ // Validate order expiration
+ // solhint-disable-next-line not-rely-on-time
+ if (block.timestamp >= order.expirationTimeSeconds) {
+ orderInfo.orderStatus = uint8(OrderStatus.EXPIRED);
+ return orderInfo;
+ }
+
+ // Check if order has been cancelled
+ if (cancelled[orderInfo.orderHash]) {
+ orderInfo.orderStatus = uint8(OrderStatus.CANCELLED);
+ return orderInfo;
+ }
+ if (orderEpoch[order.makerAddress][order.senderAddress] > order.salt) {
+ orderInfo.orderStatus = uint8(OrderStatus.CANCELLED);
+ return orderInfo;
+ }
+
+ // All other statuses are ruled out: order is Fillable
+ orderInfo.orderStatus = uint8(OrderStatus.FILLABLE);
+ return orderInfo;
+ }
+
+ /// @dev Fills the input order.
+ /// @param order Order struct containing order specifications.
+ /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
+ /// @param signature Proof that order has been created by maker.
+ /// @return Amounts filled and fees paid by maker and taker.
+ function fillOrderInternal(
+ Order memory order,
+ uint256 takerAssetFillAmount,
+ bytes memory signature
+ )
+ internal
+ returns (FillResults memory fillResults)
+ {
+ // Fetch order info
+ OrderInfo memory orderInfo = getOrderInfo(order);
+
+ // Fetch taker address
+ address takerAddress = getCurrentContextAddress();
+
+ // Assert that the order is fillable by taker
+ assertFillableOrder(
+ order,
+ orderInfo,
+ takerAddress,
+ signature
+ );
+
+ // Get amount of takerAsset to fill
+ uint256 remainingTakerAssetAmount = safeSub(order.takerAssetAmount, orderInfo.orderTakerAssetFilledAmount);
+ uint256 takerAssetFilledAmount = min256(takerAssetFillAmount, remainingTakerAssetAmount);
+
+ // Validate context
+ assertValidFill(
+ order,
+ orderInfo,
+ takerAssetFillAmount,
+ takerAssetFilledAmount,
+ fillResults.makerAssetFilledAmount
+ );
+
+ // Compute proportional fill amounts
+ fillResults = calculateFillResults(order, takerAssetFilledAmount);
+
+ // Update exchange internal state
+ updateFilledState(
+ order,
+ takerAddress,
+ orderInfo.orderHash,
+ orderInfo.orderTakerAssetFilledAmount,
+ fillResults
+ );
+
+ // Settle order
+ settleOrder(
+ order,
+ takerAddress,
+ fillResults
+ );
+
+ return fillResults;
+ }
+
+ /// @dev After calling, the order can not be filled anymore.
+ /// Throws if order is invalid or sender does not have permission to cancel.
+ /// @param order Order to cancel. Order must be OrderStatus.FILLABLE.
+ function cancelOrderInternal(Order memory order)
+ internal
+ {
+ // Fetch current order status
+ OrderInfo memory orderInfo = getOrderInfo(order);
+
+ // Validate context
+ assertValidCancel(order, orderInfo);
+
+ // Perform cancel
+ updateCancelledState(order, orderInfo.orderHash);
+ }
+
+ /// @dev Updates state with results of a fill order.
+ /// @param order that was filled.
+ /// @param takerAddress Address of taker who filled the order.
+ /// @param orderTakerAssetFilledAmount Amount of order already filled.
+ function updateFilledState(
+ Order memory order,
+ address takerAddress,
+ bytes32 orderHash,
+ uint256 orderTakerAssetFilledAmount,
+ FillResults memory fillResults
+ )
+ internal
+ {
+ // Update state
+ filled[orderHash] = safeAdd(orderTakerAssetFilledAmount, fillResults.takerAssetFilledAmount);
+
+ // Log order
+ emit Fill(
+ order.makerAddress,
+ order.feeRecipientAddress,
+ takerAddress,
+ msg.sender,
+ fillResults.makerAssetFilledAmount,
+ fillResults.takerAssetFilledAmount,
+ fillResults.makerFeePaid,
+ fillResults.takerFeePaid,
+ orderHash,
+ order.makerAssetData,
+ order.takerAssetData
+ );
+ }
+
+ /// @dev Updates state with results of cancelling an order.
+ /// State is only updated if the order is currently fillable.
+ /// Otherwise, updating state would have no effect.
+ /// @param order that was cancelled.
+ /// @param orderHash Hash of order that was cancelled.
+ function updateCancelledState(
+ Order memory order,
+ bytes32 orderHash
+ )
+ internal
+ {
+ // Perform cancel
+ cancelled[orderHash] = true;
+
+ // Log cancel
+ emit Cancel(
+ order.makerAddress,
+ order.feeRecipientAddress,
+ msg.sender,
+ orderHash,
+ order.makerAssetData,
+ order.takerAssetData
+ );
+ }
+
+ /// @dev Validates context for fillOrder. Succeeds or throws.
+ /// @param order to be filled.
+ /// @param orderInfo OrderStatus, orderHash, and amount already filled of order.
+ /// @param takerAddress Address of order taker.
+ /// @param signature Proof that the orders was created by its maker.
+ function assertFillableOrder(
+ Order memory order,
+ OrderInfo memory orderInfo,
+ address takerAddress,
+ bytes memory signature
+ )
+ internal
+ view
+ {
+ // An order can only be filled if its status is FILLABLE.
+ require(
+ orderInfo.orderStatus == uint8(OrderStatus.FILLABLE),
+ "ORDER_UNFILLABLE"
+ );
+
+ // Validate sender is allowed to fill this order
+ if (order.senderAddress != address(0)) {
+ require(
+ order.senderAddress == msg.sender,
+ "INVALID_SENDER"
+ );
+ }
+
+ // Validate taker is allowed to fill this order
+ if (order.takerAddress != address(0)) {
+ require(
+ order.takerAddress == takerAddress,
+ "INVALID_TAKER"
+ );
+ }
+
+ // Validate Maker signature (check only if first time seen)
+ if (orderInfo.orderTakerAssetFilledAmount == 0) {
+ require(
+ isValidSignature(
+ orderInfo.orderHash,
+ order.makerAddress,
+ signature
+ ),
+ "INVALID_ORDER_SIGNATURE"
+ );
+ }
+ }
+
+ /// @dev Validates context for fillOrder. Succeeds or throws.
+ /// @param order to be filled.
+ /// @param orderInfo OrderStatus, orderHash, and amount already filled of order.
+ /// @param takerAssetFillAmount Desired amount of order to fill by taker.
+ /// @param takerAssetFilledAmount Amount of takerAsset that will be filled.
+ /// @param makerAssetFilledAmount Amount of makerAsset that will be transfered.
+ function assertValidFill(
+ Order memory order,
+ OrderInfo memory orderInfo,
+ uint256 takerAssetFillAmount, // TODO: use FillResults
+ uint256 takerAssetFilledAmount,
+ uint256 makerAssetFilledAmount
+ )
+ internal
+ view
+ {
+ // Revert if fill amount is invalid
+ // TODO: reconsider necessity for v2.1
+ require(
+ takerAssetFillAmount != 0,
+ "INVALID_TAKER_AMOUNT"
+ );
+
+ // Make sure taker does not pay more than desired amount
+ // NOTE: This assertion should never fail, it is here
+ // as an extra defence against potential bugs.
+ require(
+ takerAssetFilledAmount <= takerAssetFillAmount,
+ "TAKER_OVERPAY"
+ );
+
+ // Make sure order is not overfilled
+ // NOTE: This assertion should never fail, it is here
+ // as an extra defence against potential bugs.
+ require(
+ safeAdd(orderInfo.orderTakerAssetFilledAmount, takerAssetFilledAmount) <= order.takerAssetAmount,
+ "ORDER_OVERFILL"
+ );
+
+ // Make sure order is filled at acceptable price.
+ // The order has an implied price from the makers perspective:
+ // order price = order.makerAssetAmount / order.takerAssetAmount
+ // i.e. the number of makerAsset maker is paying per takerAsset. The
+ // maker is guaranteed to get this price or a better (lower) one. The
+ // actual price maker is getting in this fill is:
+ // fill price = makerAssetFilledAmount / takerAssetFilledAmount
+ // We need `fill price <= order price` for the fill to be fair to maker.
+ // This amounts to:
+ // makerAssetFilledAmount order.makerAssetAmount
+ // ------------------------ <= -----------------------
+ // takerAssetFilledAmount order.takerAssetAmount
+ // or, equivalently:
+ // makerAssetFilledAmount * order.takerAssetAmount <=
+ // order.makerAssetAmount * takerAssetFilledAmount
+ // NOTE: This assertion should never fail, it is here
+ // as an extra defence against potential bugs.
+ require(
+ safeMul(makerAssetFilledAmount, order.takerAssetAmount)
+ <=
+ safeMul(order.makerAssetAmount, takerAssetFilledAmount),
+ "INVALID_FILL_PRICE"
+ );
+ }
+
+ /// @dev Validates context for cancelOrder. Succeeds or throws.
+ /// @param order to be cancelled.
+ /// @param orderInfo OrderStatus, orderHash, and amount already filled of order.
+ function assertValidCancel(
+ Order memory order,
+ OrderInfo memory orderInfo
+ )
+ internal
+ view
+ {
+ // Ensure order is valid
+ // An order can only be cancelled if its status is FILLABLE.
+ require(
+ orderInfo.orderStatus == uint8(OrderStatus.FILLABLE),
+ "ORDER_UNFILLABLE"
+ );
+
+ // Validate sender is allowed to cancel this order
+ if (order.senderAddress != address(0)) {
+ require(
+ order.senderAddress == msg.sender,
+ "INVALID_SENDER"
+ );
+ }
+
+ // Validate transaction signed by maker
+ address makerAddress = getCurrentContextAddress();
+ require(
+ order.makerAddress == makerAddress,
+ "INVALID_MAKER"
+ );
+ }
+
+ /// @dev Calculates amounts filled and fees paid by maker and taker.
+ /// @param order to be filled.
+ /// @param takerAssetFilledAmount Amount of takerAsset that will be filled.
+ /// @return fillResults Amounts filled and fees paid by maker and taker.
+ function calculateFillResults(
+ Order memory order,
+ uint256 takerAssetFilledAmount
+ )
+ internal
+ pure
+ returns (FillResults memory fillResults)
+ {
+ // Compute proportional transfer amounts
+ fillResults.takerAssetFilledAmount = takerAssetFilledAmount;
+ fillResults.makerAssetFilledAmount = safeGetPartialAmountFloor(
+ takerAssetFilledAmount,
+ order.takerAssetAmount,
+ order.makerAssetAmount
+ );
+ fillResults.makerFeePaid = safeGetPartialAmountFloor(
+ fillResults.makerAssetFilledAmount,
+ order.makerAssetAmount,
+ order.makerFee
+ );
+ fillResults.takerFeePaid = safeGetPartialAmountFloor(
+ takerAssetFilledAmount,
+ order.takerAssetAmount,
+ order.takerFee
+ );
+
+ return fillResults;
+ }
+
+ /// @dev Settles an order by transferring assets between counterparties.
+ /// @param order Order struct containing order specifications.
+ /// @param takerAddress Address selling takerAsset and buying makerAsset.
+ /// @param fillResults Amounts to be filled and fees paid by maker and taker.
+ function settleOrder(
+ LibOrder.Order memory order,
+ address takerAddress,
+ LibFillResults.FillResults memory fillResults
+ )
+ private
+ {
+ bytes memory zrxAssetData = ZRX_ASSET_DATA;
+ dispatchTransferFrom(
+ order.makerAssetData,
+ order.makerAddress,
+ takerAddress,
+ fillResults.makerAssetFilledAmount
+ );
+ dispatchTransferFrom(
+ order.takerAssetData,
+ takerAddress,
+ order.makerAddress,
+ fillResults.takerAssetFilledAmount
+ );
+ dispatchTransferFrom(
+ zrxAssetData,
+ order.makerAddress,
+ order.feeRecipientAddress,
+ fillResults.makerFeePaid
+ );
+ dispatchTransferFrom(
+ zrxAssetData,
+ takerAddress,
+ order.feeRecipientAddress,
+ fillResults.takerFeePaid
+ );
+ }
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/MixinMatchOrders.sol b/contracts/protocol/contracts/protocol/Exchange/MixinMatchOrders.sol
new file mode 100644
index 000000000..fc6d73482
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/MixinMatchOrders.sol
@@ -0,0 +1,335 @@
+/*
+ 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;
+pragma experimental ABIEncoderV2;
+
+import "@0x/contracts-utils/contracts/utils/ReentrancyGuard/ReentrancyGuard.sol";
+import "@0x/contracts-libs/contracts/libs/LibConstants.sol";
+import "@0x/contracts-libs/contracts/libs/LibMath.sol";
+import "@0x/contracts-libs/contracts/libs/LibOrder.sol";
+import "@0x/contracts-libs/contracts/libs/LibFillResults.sol";
+import "./mixins/MExchangeCore.sol";
+import "./mixins/MMatchOrders.sol";
+import "./mixins/MTransactions.sol";
+import "./mixins/MAssetProxyDispatcher.sol";
+
+
+contract MixinMatchOrders is
+ ReentrancyGuard,
+ LibConstants,
+ LibMath,
+ MAssetProxyDispatcher,
+ MExchangeCore,
+ MMatchOrders,
+ MTransactions
+{
+ /// @dev Match two complementary orders that have a profitable spread.
+ /// Each order is filled at their respective price point. However, the calculations are
+ /// carried out as though the orders are both being filled at the right order's price point.
+ /// The profit made by the left order goes to the taker (who matched the two orders).
+ /// @param leftOrder First order to match.
+ /// @param rightOrder Second order to match.
+ /// @param leftSignature Proof that order was created by the left maker.
+ /// @param rightSignature Proof that order was created by the right maker.
+ /// @return matchedFillResults Amounts filled and fees paid by maker and taker of matched orders.
+ function matchOrders(
+ LibOrder.Order memory leftOrder,
+ LibOrder.Order memory rightOrder,
+ bytes memory leftSignature,
+ bytes memory rightSignature
+ )
+ public
+ nonReentrant
+ returns (LibFillResults.MatchedFillResults memory matchedFillResults)
+ {
+ // We assume that rightOrder.takerAssetData == leftOrder.makerAssetData and rightOrder.makerAssetData == leftOrder.takerAssetData.
+ // If this assumption isn't true, the match will fail at signature validation.
+ rightOrder.makerAssetData = leftOrder.takerAssetData;
+ rightOrder.takerAssetData = leftOrder.makerAssetData;
+
+ // Get left & right order info
+ LibOrder.OrderInfo memory leftOrderInfo = getOrderInfo(leftOrder);
+ LibOrder.OrderInfo memory rightOrderInfo = getOrderInfo(rightOrder);
+
+ // Fetch taker address
+ address takerAddress = getCurrentContextAddress();
+
+ // Either our context is valid or we revert
+ assertFillableOrder(
+ leftOrder,
+ leftOrderInfo,
+ takerAddress,
+ leftSignature
+ );
+ assertFillableOrder(
+ rightOrder,
+ rightOrderInfo,
+ takerAddress,
+ rightSignature
+ );
+ assertValidMatch(leftOrder, rightOrder);
+
+ // Compute proportional fill amounts
+ matchedFillResults = calculateMatchedFillResults(
+ leftOrder,
+ rightOrder,
+ leftOrderInfo.orderTakerAssetFilledAmount,
+ rightOrderInfo.orderTakerAssetFilledAmount
+ );
+
+ // Validate fill contexts
+ assertValidFill(
+ leftOrder,
+ leftOrderInfo,
+ matchedFillResults.left.takerAssetFilledAmount,
+ matchedFillResults.left.takerAssetFilledAmount,
+ matchedFillResults.left.makerAssetFilledAmount
+ );
+ assertValidFill(
+ rightOrder,
+ rightOrderInfo,
+ matchedFillResults.right.takerAssetFilledAmount,
+ matchedFillResults.right.takerAssetFilledAmount,
+ matchedFillResults.right.makerAssetFilledAmount
+ );
+
+ // Update exchange state
+ updateFilledState(
+ leftOrder,
+ takerAddress,
+ leftOrderInfo.orderHash,
+ leftOrderInfo.orderTakerAssetFilledAmount,
+ matchedFillResults.left
+ );
+ updateFilledState(
+ rightOrder,
+ takerAddress,
+ rightOrderInfo.orderHash,
+ rightOrderInfo.orderTakerAssetFilledAmount,
+ matchedFillResults.right
+ );
+
+ // Settle matched orders. Succeeds or throws.
+ settleMatchedOrders(
+ leftOrder,
+ rightOrder,
+ takerAddress,
+ matchedFillResults
+ );
+
+ return matchedFillResults;
+ }
+
+ /// @dev Validates context for matchOrders. Succeeds or throws.
+ /// @param leftOrder First order to match.
+ /// @param rightOrder Second order to match.
+ function assertValidMatch(
+ LibOrder.Order memory leftOrder,
+ LibOrder.Order memory rightOrder
+ )
+ internal
+ pure
+ {
+ // Make sure there is a profitable spread.
+ // There is a profitable spread iff the cost per unit bought (OrderA.MakerAmount/OrderA.TakerAmount) for each order is greater
+ // than the profit per unit sold of the matched order (OrderB.TakerAmount/OrderB.MakerAmount).
+ // This is satisfied by the equations below:
+ // <leftOrder.makerAssetAmount> / <leftOrder.takerAssetAmount> >= <rightOrder.takerAssetAmount> / <rightOrder.makerAssetAmount>
+ // AND
+ // <rightOrder.makerAssetAmount> / <rightOrder.takerAssetAmount> >= <leftOrder.takerAssetAmount> / <leftOrder.makerAssetAmount>
+ // These equations can be combined to get the following:
+ require(
+ safeMul(leftOrder.makerAssetAmount, rightOrder.makerAssetAmount) >=
+ safeMul(leftOrder.takerAssetAmount, rightOrder.takerAssetAmount),
+ "NEGATIVE_SPREAD_REQUIRED"
+ );
+ }
+
+ /// @dev Calculates fill amounts for the matched orders.
+ /// Each order is filled at their respective price point. However, the calculations are
+ /// carried out as though the orders are both being filled at the right order's price point.
+ /// The profit made by the leftOrder order goes to the taker (who matched the two orders).
+ /// @param leftOrder First order to match.
+ /// @param rightOrder Second order to match.
+ /// @param leftOrderTakerAssetFilledAmount Amount of left order already filled.
+ /// @param rightOrderTakerAssetFilledAmount Amount of right order already filled.
+ /// @param matchedFillResults Amounts to fill and fees to pay by maker and taker of matched orders.
+ function calculateMatchedFillResults(
+ LibOrder.Order memory leftOrder,
+ LibOrder.Order memory rightOrder,
+ uint256 leftOrderTakerAssetFilledAmount,
+ uint256 rightOrderTakerAssetFilledAmount
+ )
+ internal
+ pure
+ returns (LibFillResults.MatchedFillResults memory matchedFillResults)
+ {
+ // Derive maker asset amounts for left & right orders, given store taker assert amounts
+ uint256 leftTakerAssetAmountRemaining = safeSub(leftOrder.takerAssetAmount, leftOrderTakerAssetFilledAmount);
+ uint256 leftMakerAssetAmountRemaining = safeGetPartialAmountFloor(
+ leftOrder.makerAssetAmount,
+ leftOrder.takerAssetAmount,
+ leftTakerAssetAmountRemaining
+ );
+ uint256 rightTakerAssetAmountRemaining = safeSub(rightOrder.takerAssetAmount, rightOrderTakerAssetFilledAmount);
+ uint256 rightMakerAssetAmountRemaining = safeGetPartialAmountFloor(
+ rightOrder.makerAssetAmount,
+ rightOrder.takerAssetAmount,
+ rightTakerAssetAmountRemaining
+ );
+
+ // Calculate fill results for maker and taker assets: at least one order will be fully filled.
+ // The maximum amount the left maker can buy is `leftTakerAssetAmountRemaining`
+ // The maximum amount the right maker can sell is `rightMakerAssetAmountRemaining`
+ // We have two distinct cases for calculating the fill results:
+ // Case 1.
+ // If the left maker can buy more than the right maker can sell, then only the right order is fully filled.
+ // If the left maker can buy exactly what the right maker can sell, then both orders are fully filled.
+ // Case 2.
+ // If the left maker cannot buy more than the right maker can sell, then only the left order is fully filled.
+ if (leftTakerAssetAmountRemaining >= rightMakerAssetAmountRemaining) {
+ // Case 1: Right order is fully filled
+ matchedFillResults.right.makerAssetFilledAmount = rightMakerAssetAmountRemaining;
+ matchedFillResults.right.takerAssetFilledAmount = rightTakerAssetAmountRemaining;
+ matchedFillResults.left.takerAssetFilledAmount = matchedFillResults.right.makerAssetFilledAmount;
+ // Round down to ensure the maker's exchange rate does not exceed the price specified by the order.
+ // We favor the maker when the exchange rate must be rounded.
+ matchedFillResults.left.makerAssetFilledAmount = safeGetPartialAmountFloor(
+ leftOrder.makerAssetAmount,
+ leftOrder.takerAssetAmount,
+ matchedFillResults.left.takerAssetFilledAmount
+ );
+ } else {
+ // Case 2: Left order is fully filled
+ matchedFillResults.left.makerAssetFilledAmount = leftMakerAssetAmountRemaining;
+ matchedFillResults.left.takerAssetFilledAmount = leftTakerAssetAmountRemaining;
+ matchedFillResults.right.makerAssetFilledAmount = matchedFillResults.left.takerAssetFilledAmount;
+ // Round up to ensure the maker's exchange rate does not exceed the price specified by the order.
+ // We favor the maker when the exchange rate must be rounded.
+ matchedFillResults.right.takerAssetFilledAmount = safeGetPartialAmountCeil(
+ rightOrder.takerAssetAmount,
+ rightOrder.makerAssetAmount,
+ matchedFillResults.right.makerAssetFilledAmount
+ );
+ }
+
+ // Calculate amount given to taker
+ matchedFillResults.leftMakerAssetSpreadAmount = safeSub(
+ matchedFillResults.left.makerAssetFilledAmount,
+ matchedFillResults.right.takerAssetFilledAmount
+ );
+
+ // Compute fees for left order
+ matchedFillResults.left.makerFeePaid = safeGetPartialAmountFloor(
+ matchedFillResults.left.makerAssetFilledAmount,
+ leftOrder.makerAssetAmount,
+ leftOrder.makerFee
+ );
+ matchedFillResults.left.takerFeePaid = safeGetPartialAmountFloor(
+ matchedFillResults.left.takerAssetFilledAmount,
+ leftOrder.takerAssetAmount,
+ leftOrder.takerFee
+ );
+
+ // Compute fees for right order
+ matchedFillResults.right.makerFeePaid = safeGetPartialAmountFloor(
+ matchedFillResults.right.makerAssetFilledAmount,
+ rightOrder.makerAssetAmount,
+ rightOrder.makerFee
+ );
+ matchedFillResults.right.takerFeePaid = safeGetPartialAmountFloor(
+ matchedFillResults.right.takerAssetFilledAmount,
+ rightOrder.takerAssetAmount,
+ rightOrder.takerFee
+ );
+
+ // Return fill results
+ return matchedFillResults;
+ }
+
+ /// @dev Settles matched order by transferring appropriate funds between order makers, taker, and fee recipient.
+ /// @param leftOrder First matched order.
+ /// @param rightOrder Second matched order.
+ /// @param takerAddress Address that matched the orders. The taker receives the spread between orders as profit.
+ /// @param matchedFillResults Struct holding amounts to transfer between makers, taker, and fee recipients.
+ function settleMatchedOrders(
+ LibOrder.Order memory leftOrder,
+ LibOrder.Order memory rightOrder,
+ address takerAddress,
+ LibFillResults.MatchedFillResults memory matchedFillResults
+ )
+ private
+ {
+ bytes memory zrxAssetData = ZRX_ASSET_DATA;
+ // Order makers and taker
+ dispatchTransferFrom(
+ leftOrder.makerAssetData,
+ leftOrder.makerAddress,
+ rightOrder.makerAddress,
+ matchedFillResults.right.takerAssetFilledAmount
+ );
+ dispatchTransferFrom(
+ rightOrder.makerAssetData,
+ rightOrder.makerAddress,
+ leftOrder.makerAddress,
+ matchedFillResults.left.takerAssetFilledAmount
+ );
+ dispatchTransferFrom(
+ leftOrder.makerAssetData,
+ leftOrder.makerAddress,
+ takerAddress,
+ matchedFillResults.leftMakerAssetSpreadAmount
+ );
+
+ // Maker fees
+ dispatchTransferFrom(
+ zrxAssetData,
+ leftOrder.makerAddress,
+ leftOrder.feeRecipientAddress,
+ matchedFillResults.left.makerFeePaid
+ );
+ dispatchTransferFrom(
+ zrxAssetData,
+ rightOrder.makerAddress,
+ rightOrder.feeRecipientAddress,
+ matchedFillResults.right.makerFeePaid
+ );
+
+ // Taker fees
+ if (leftOrder.feeRecipientAddress == rightOrder.feeRecipientAddress) {
+ dispatchTransferFrom(
+ zrxAssetData,
+ takerAddress,
+ leftOrder.feeRecipientAddress,
+ safeAdd(
+ matchedFillResults.left.takerFeePaid,
+ matchedFillResults.right.takerFeePaid
+ )
+ );
+ } else {
+ dispatchTransferFrom(
+ zrxAssetData,
+ takerAddress,
+ leftOrder.feeRecipientAddress,
+ matchedFillResults.left.takerFeePaid
+ );
+ dispatchTransferFrom(
+ zrxAssetData,
+ takerAddress,
+ rightOrder.feeRecipientAddress,
+ matchedFillResults.right.takerFeePaid
+ );
+ }
+ }
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/MixinSignatureValidator.sol b/contracts/protocol/contracts/protocol/Exchange/MixinSignatureValidator.sol
new file mode 100644
index 000000000..80b4c0755
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/MixinSignatureValidator.sol
@@ -0,0 +1,324 @@
+/*
+
+ 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;
+
+import "@0x/contracts-utils/contracts/utils/LibBytes/LibBytes.sol";
+import "@0x/contracts-utils/contracts/utils/ReentrancyGuard/ReentrancyGuard.sol";
+import "./mixins/MSignatureValidator.sol";
+import "./mixins/MTransactions.sol";
+import "@0x/contracts-interfaces/contracts/protocol/Exchange/IWallet.sol";
+import "@0x/contracts-interfaces/contracts/protocol/Exchange/IValidator.sol";
+
+
+contract MixinSignatureValidator is
+ ReentrancyGuard,
+ MSignatureValidator,
+ MTransactions
+{
+ using LibBytes for bytes;
+
+ // Mapping of hash => signer => signed
+ mapping (bytes32 => mapping (address => bool)) public preSigned;
+
+ // Mapping of signer => validator => approved
+ mapping (address => mapping (address => bool)) public allowedValidators;
+
+ /// @dev Approves a hash on-chain using any valid signature type.
+ /// After presigning a hash, the preSign signature type will become valid for that hash and signer.
+ /// @param signerAddress Address that should have signed the given hash.
+ /// @param signature Proof that the hash has been signed by signer.
+ function preSign(
+ bytes32 hash,
+ address signerAddress,
+ bytes signature
+ )
+ external
+ {
+ if (signerAddress != msg.sender) {
+ require(
+ isValidSignature(
+ hash,
+ signerAddress,
+ signature
+ ),
+ "INVALID_SIGNATURE"
+ );
+ }
+ preSigned[hash][signerAddress] = true;
+ }
+
+ /// @dev Approves/unnapproves a Validator contract to verify signatures on signer's behalf.
+ /// @param validatorAddress Address of Validator contract.
+ /// @param approval Approval or disapproval of Validator contract.
+ function setSignatureValidatorApproval(
+ address validatorAddress,
+ bool approval
+ )
+ external
+ nonReentrant
+ {
+ address signerAddress = getCurrentContextAddress();
+ allowedValidators[signerAddress][validatorAddress] = approval;
+ emit SignatureValidatorApproval(
+ signerAddress,
+ validatorAddress,
+ approval
+ );
+ }
+
+ /// @dev Verifies that a hash has been signed by the given signer.
+ /// @param hash Any 32 byte hash.
+ /// @param signerAddress Address that should have signed the given hash.
+ /// @param signature Proof that the hash has been signed by signer.
+ /// @return True if the address recovered from the provided signature matches the input signer address.
+ function isValidSignature(
+ bytes32 hash,
+ address signerAddress,
+ bytes memory signature
+ )
+ public
+ view
+ returns (bool isValid)
+ {
+ require(
+ signature.length > 0,
+ "LENGTH_GREATER_THAN_0_REQUIRED"
+ );
+
+ // Pop last byte off of signature byte array.
+ uint8 signatureTypeRaw = uint8(signature.popLastByte());
+
+ // Ensure signature is supported
+ require(
+ signatureTypeRaw < uint8(SignatureType.NSignatureTypes),
+ "SIGNATURE_UNSUPPORTED"
+ );
+
+ SignatureType signatureType = SignatureType(signatureTypeRaw);
+
+ // Variables are not scoped in Solidity.
+ uint8 v;
+ bytes32 r;
+ bytes32 s;
+ address recovered;
+
+ // Always illegal signature.
+ // This is always an implicit option since a signer can create a
+ // signature array with invalid type or length. We may as well make
+ // it an explicit option. This aids testing and analysis. It is
+ // also the initialization value for the enum type.
+ if (signatureType == SignatureType.Illegal) {
+ revert("SIGNATURE_ILLEGAL");
+
+ // Always invalid signature.
+ // Like Illegal, this is always implicitly available and therefore
+ // offered explicitly. It can be implicitly created by providing
+ // a correctly formatted but incorrect signature.
+ } else if (signatureType == SignatureType.Invalid) {
+ require(
+ signature.length == 0,
+ "LENGTH_0_REQUIRED"
+ );
+ isValid = false;
+ return isValid;
+
+ // Signature using EIP712
+ } else if (signatureType == SignatureType.EIP712) {
+ require(
+ signature.length == 65,
+ "LENGTH_65_REQUIRED"
+ );
+ v = uint8(signature[0]);
+ r = signature.readBytes32(1);
+ s = signature.readBytes32(33);
+ recovered = ecrecover(
+ hash,
+ v,
+ r,
+ s
+ );
+ isValid = signerAddress == recovered;
+ return isValid;
+
+ // Signed using web3.eth_sign
+ } else if (signatureType == SignatureType.EthSign) {
+ require(
+ signature.length == 65,
+ "LENGTH_65_REQUIRED"
+ );
+ v = uint8(signature[0]);
+ r = signature.readBytes32(1);
+ s = signature.readBytes32(33);
+ recovered = ecrecover(
+ keccak256(abi.encodePacked(
+ "\x19Ethereum Signed Message:\n32",
+ hash
+ )),
+ v,
+ r,
+ s
+ );
+ isValid = signerAddress == recovered;
+ return isValid;
+
+ // Signature verified by wallet contract.
+ // If used with an order, the maker of the order is the wallet contract.
+ } else if (signatureType == SignatureType.Wallet) {
+ isValid = isValidWalletSignature(
+ hash,
+ signerAddress,
+ signature
+ );
+ return isValid;
+
+ // Signature verified by validator contract.
+ // If used with an order, the maker of the order can still be an EOA.
+ // A signature using this type should be encoded as:
+ // | Offset | Length | Contents |
+ // | 0x00 | x | Signature to validate |
+ // | 0x00 + x | 20 | Address of validator contract |
+ // | 0x14 + x | 1 | Signature type is always "\x06" |
+ } else if (signatureType == SignatureType.Validator) {
+ // Pop last 20 bytes off of signature byte array.
+ address validatorAddress = signature.popLast20Bytes();
+
+ // Ensure signer has approved validator.
+ if (!allowedValidators[signerAddress][validatorAddress]) {
+ return false;
+ }
+ isValid = isValidValidatorSignature(
+ validatorAddress,
+ hash,
+ signerAddress,
+ signature
+ );
+ return isValid;
+
+ // Signer signed hash previously using the preSign function.
+ } else if (signatureType == SignatureType.PreSigned) {
+ isValid = preSigned[hash][signerAddress];
+ return isValid;
+ }
+
+ // Anything else is illegal (We do not return false because
+ // the signature may actually be valid, just not in a format
+ // that we currently support. In this case returning false
+ // may lead the caller to incorrectly believe that the
+ // signature was invalid.)
+ revert("SIGNATURE_UNSUPPORTED");
+ }
+
+ /// @dev Verifies signature using logic defined by Wallet contract.
+ /// @param hash Any 32 byte hash.
+ /// @param walletAddress Address that should have signed the given hash
+ /// and defines its own signature verification method.
+ /// @param signature Proof that the hash has been signed by signer.
+ /// @return True if signature is valid for given wallet..
+ function isValidWalletSignature(
+ bytes32 hash,
+ address walletAddress,
+ bytes signature
+ )
+ internal
+ view
+ returns (bool isValid)
+ {
+ bytes memory callData = abi.encodeWithSelector(
+ IWallet(walletAddress).isValidSignature.selector,
+ hash,
+ signature
+ );
+ assembly {
+ let cdStart := add(callData, 32)
+ let success := staticcall(
+ gas, // forward all gas
+ walletAddress, // address of Wallet contract
+ cdStart, // pointer to start of input
+ mload(callData), // length of input
+ cdStart, // write output over input
+ 32 // output size is 32 bytes
+ )
+
+ switch success
+ case 0 {
+ // Revert with `Error("WALLET_ERROR")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000000c57414c4c45545f4552524f5200000000000000000000000000000000)
+ mstore(96, 0)
+ revert(0, 100)
+ }
+ case 1 {
+ // Signature is valid if call did not revert and returned true
+ isValid := mload(cdStart)
+ }
+ }
+ return isValid;
+ }
+
+ /// @dev Verifies signature using logic defined by Validator contract.
+ /// @param validatorAddress Address of validator contract.
+ /// @param hash Any 32 byte hash.
+ /// @param signerAddress Address that should have signed the given hash.
+ /// @param signature Proof that the hash has been signed by signer.
+ /// @return True if the address recovered from the provided signature matches the input signer address.
+ function isValidValidatorSignature(
+ address validatorAddress,
+ bytes32 hash,
+ address signerAddress,
+ bytes signature
+ )
+ internal
+ view
+ returns (bool isValid)
+ {
+ bytes memory callData = abi.encodeWithSelector(
+ IValidator(signerAddress).isValidSignature.selector,
+ hash,
+ signerAddress,
+ signature
+ );
+ assembly {
+ let cdStart := add(callData, 32)
+ let success := staticcall(
+ gas, // forward all gas
+ validatorAddress, // address of Validator contract
+ cdStart, // pointer to start of input
+ mload(callData), // length of input
+ cdStart, // write output over input
+ 32 // output size is 32 bytes
+ )
+
+ switch success
+ case 0 {
+ // Revert with `Error("VALIDATOR_ERROR")`
+ mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000)
+ mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000)
+ mstore(64, 0x0000000f56414c494441544f525f4552524f5200000000000000000000000000)
+ mstore(96, 0)
+ revert(0, 100)
+ }
+ case 1 {
+ // Signature is valid if call did not revert and returned true
+ isValid := mload(cdStart)
+ }
+ }
+ return isValid;
+ }
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/MixinTransactions.sol b/contracts/protocol/contracts/protocol/Exchange/MixinTransactions.sol
new file mode 100644
index 000000000..87c614382
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/MixinTransactions.sol
@@ -0,0 +1,152 @@
+/*
+
+ 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;
+
+import "@0x/contracts-libs/contracts/libs/LibExchangeErrors.sol";
+import "./mixins/MSignatureValidator.sol";
+import "./mixins/MTransactions.sol";
+import "@0x/contracts-libs/contracts/libs/LibEIP712.sol";
+
+
+contract MixinTransactions is
+ LibEIP712,
+ MSignatureValidator,
+ MTransactions
+{
+ // Mapping of transaction hash => executed
+ // This prevents transactions from being executed more than once.
+ mapping (bytes32 => bool) public transactions;
+
+ // Address of current transaction signer
+ address public currentContextAddress;
+
+ /// @dev Executes an exchange method call in the context of signer.
+ /// @param salt Arbitrary number to ensure uniqueness of transaction hash.
+ /// @param signerAddress Address of transaction signer.
+ /// @param data AbiV2 encoded calldata.
+ /// @param signature Proof of signer transaction by signer.
+ function executeTransaction(
+ uint256 salt,
+ address signerAddress,
+ bytes data,
+ bytes signature
+ )
+ external
+ {
+ // Prevent reentrancy
+ require(
+ currentContextAddress == address(0),
+ "REENTRANCY_ILLEGAL"
+ );
+
+ bytes32 transactionHash = hashEIP712Message(hashZeroExTransaction(
+ salt,
+ signerAddress,
+ data
+ ));
+
+ // Validate transaction has not been executed
+ require(
+ !transactions[transactionHash],
+ "INVALID_TX_HASH"
+ );
+
+ // Transaction always valid if signer is sender of transaction
+ if (signerAddress != msg.sender) {
+ // Validate signature
+ require(
+ isValidSignature(
+ transactionHash,
+ signerAddress,
+ signature
+ ),
+ "INVALID_TX_SIGNATURE"
+ );
+
+ // Set the current transaction signer
+ currentContextAddress = signerAddress;
+ }
+
+ // Execute transaction
+ transactions[transactionHash] = true;
+ require(
+ address(this).delegatecall(data),
+ "FAILED_EXECUTION"
+ );
+
+ // Reset current transaction signer if it was previously updated
+ if (signerAddress != msg.sender) {
+ currentContextAddress = address(0);
+ }
+ }
+
+ /// @dev Calculates EIP712 hash of the Transaction.
+ /// @param salt Arbitrary number to ensure uniqueness of transaction hash.
+ /// @param signerAddress Address of transaction signer.
+ /// @param data AbiV2 encoded calldata.
+ /// @return EIP712 hash of the Transaction.
+ function hashZeroExTransaction(
+ uint256 salt,
+ address signerAddress,
+ bytes memory data
+ )
+ internal
+ pure
+ returns (bytes32 result)
+ {
+ bytes32 schemaHash = EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH;
+ bytes32 dataHash = keccak256(data);
+
+ // Assembly for more efficiently computing:
+ // keccak256(abi.encodePacked(
+ // EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH,
+ // salt,
+ // bytes32(signerAddress),
+ // keccak256(data)
+ // ));
+
+ assembly {
+ // Load free memory pointer
+ let memPtr := mload(64)
+
+ mstore(memPtr, schemaHash) // hash of schema
+ mstore(add(memPtr, 32), salt) // salt
+ mstore(add(memPtr, 64), and(signerAddress, 0xffffffffffffffffffffffffffffffffffffffff)) // signerAddress
+ mstore(add(memPtr, 96), dataHash) // hash of data
+
+ // Compute hash
+ result := keccak256(memPtr, 128)
+ }
+ return result;
+ }
+
+ /// @dev The current function will be called in the context of this address (either 0x transaction signer or `msg.sender`).
+ /// If calling a fill function, this address will represent the taker.
+ /// If calling a cancel function, this address will represent the maker.
+ /// @return Signer of 0x transaction if entry point is `executeTransaction`.
+ /// `msg.sender` if entry point is any other function.
+ function getCurrentContextAddress()
+ internal
+ view
+ returns (address)
+ {
+ address currentContextAddress_ = currentContextAddress;
+ address contextAddress = currentContextAddress_ == address(0) ? msg.sender : currentContextAddress_;
+ return contextAddress;
+ }
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/MixinWrapperFunctions.sol b/contracts/protocol/contracts/protocol/Exchange/MixinWrapperFunctions.sol
new file mode 100644
index 000000000..2d43432ff
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/MixinWrapperFunctions.sol
@@ -0,0 +1,426 @@
+/*
+
+ 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;
+pragma experimental ABIEncoderV2;
+
+import "@0x/contracts-utils/contracts/utils/ReentrancyGuard/ReentrancyGuard.sol";
+import "@0x/contracts-libs/contracts/libs/LibMath.sol";
+import "@0x/contracts-libs/contracts/libs/LibOrder.sol";
+import "@0x/contracts-libs/contracts/libs/LibFillResults.sol";
+import "@0x/contracts-libs/contracts/libs/LibAbiEncoder.sol";
+import "./mixins/MExchangeCore.sol";
+import "./mixins/MWrapperFunctions.sol";
+
+
+contract MixinWrapperFunctions is
+ ReentrancyGuard,
+ LibMath,
+ LibFillResults,
+ LibAbiEncoder,
+ MExchangeCore,
+ MWrapperFunctions
+{
+ /// @dev Fills the input order. Reverts if exact takerAssetFillAmount not filled.
+ /// @param order Order struct containing order specifications.
+ /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
+ /// @param signature Proof that order has been created by maker.
+ function fillOrKillOrder(
+ LibOrder.Order memory order,
+ uint256 takerAssetFillAmount,
+ bytes memory signature
+ )
+ public
+ nonReentrant
+ returns (FillResults memory fillResults)
+ {
+ fillResults = fillOrKillOrderInternal(
+ order,
+ takerAssetFillAmount,
+ signature
+ );
+ return fillResults;
+ }
+
+ /// @dev Fills the input order.
+ /// Returns false if the transaction would otherwise revert.
+ /// @param order Order struct containing order specifications.
+ /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
+ /// @param signature Proof that order has been created by maker.
+ /// @return Amounts filled and fees paid by maker and taker.
+ function fillOrderNoThrow(
+ LibOrder.Order memory order,
+ uint256 takerAssetFillAmount,
+ bytes memory signature
+ )
+ public
+ returns (FillResults memory fillResults)
+ {
+ // ABI encode calldata for `fillOrder`
+ bytes memory fillOrderCalldata = abiEncodeFillOrder(
+ order,
+ takerAssetFillAmount,
+ signature
+ );
+
+ // Delegate to `fillOrder` and handle any exceptions gracefully
+ assembly {
+ let success := delegatecall(
+ gas, // forward all gas
+ address, // call address of this contract
+ add(fillOrderCalldata, 32), // pointer to start of input (skip array length in first 32 bytes)
+ mload(fillOrderCalldata), // length of input
+ fillOrderCalldata, // write output over input
+ 128 // output size is 128 bytes
+ )
+ if success {
+ mstore(fillResults, mload(fillOrderCalldata))
+ mstore(add(fillResults, 32), mload(add(fillOrderCalldata, 32)))
+ mstore(add(fillResults, 64), mload(add(fillOrderCalldata, 64)))
+ mstore(add(fillResults, 96), mload(add(fillOrderCalldata, 96)))
+ }
+ }
+ // fillResults values will be 0 by default if call was unsuccessful
+ return fillResults;
+ }
+
+ /// @dev Synchronously executes multiple calls of fillOrder.
+ /// @param orders Array of order specifications.
+ /// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders.
+ /// @param signatures Proofs that orders have been created by makers.
+ /// @return Amounts filled and fees paid by makers and taker.
+ /// NOTE: makerAssetFilledAmount and takerAssetFilledAmount may include amounts filled of different assets.
+ function batchFillOrders(
+ LibOrder.Order[] memory orders,
+ uint256[] memory takerAssetFillAmounts,
+ bytes[] memory signatures
+ )
+ public
+ nonReentrant
+ returns (FillResults memory totalFillResults)
+ {
+ uint256 ordersLength = orders.length;
+ for (uint256 i = 0; i != ordersLength; i++) {
+ FillResults memory singleFillResults = fillOrderInternal(
+ orders[i],
+ takerAssetFillAmounts[i],
+ signatures[i]
+ );
+ addFillResults(totalFillResults, singleFillResults);
+ }
+ return totalFillResults;
+ }
+
+ /// @dev Synchronously executes multiple calls of fillOrKill.
+ /// @param orders Array of order specifications.
+ /// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders.
+ /// @param signatures Proofs that orders have been created by makers.
+ /// @return Amounts filled and fees paid by makers and taker.
+ /// NOTE: makerAssetFilledAmount and takerAssetFilledAmount may include amounts filled of different assets.
+ function batchFillOrKillOrders(
+ LibOrder.Order[] memory orders,
+ uint256[] memory takerAssetFillAmounts,
+ bytes[] memory signatures
+ )
+ public
+ nonReentrant
+ returns (FillResults memory totalFillResults)
+ {
+ uint256 ordersLength = orders.length;
+ for (uint256 i = 0; i != ordersLength; i++) {
+ FillResults memory singleFillResults = fillOrKillOrderInternal(
+ orders[i],
+ takerAssetFillAmounts[i],
+ signatures[i]
+ );
+ addFillResults(totalFillResults, singleFillResults);
+ }
+ return totalFillResults;
+ }
+
+ /// @dev Fills an order with specified parameters and ECDSA signature.
+ /// Returns false if the transaction would otherwise revert.
+ /// @param orders Array of order specifications.
+ /// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders.
+ /// @param signatures Proofs that orders have been created by makers.
+ /// @return Amounts filled and fees paid by makers and taker.
+ /// NOTE: makerAssetFilledAmount and takerAssetFilledAmount may include amounts filled of different assets.
+ function batchFillOrdersNoThrow(
+ LibOrder.Order[] memory orders,
+ uint256[] memory takerAssetFillAmounts,
+ bytes[] memory signatures
+ )
+ public
+ returns (FillResults memory totalFillResults)
+ {
+ uint256 ordersLength = orders.length;
+ for (uint256 i = 0; i != ordersLength; i++) {
+ FillResults memory singleFillResults = fillOrderNoThrow(
+ orders[i],
+ takerAssetFillAmounts[i],
+ signatures[i]
+ );
+ addFillResults(totalFillResults, singleFillResults);
+ }
+ return totalFillResults;
+ }
+
+ /// @dev Synchronously executes multiple calls of fillOrder until total amount of takerAsset is sold by taker.
+ /// @param orders Array of order specifications.
+ /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
+ /// @param signatures Proofs that orders have been created by makers.
+ /// @return Amounts filled and fees paid by makers and taker.
+ function marketSellOrders(
+ LibOrder.Order[] memory orders,
+ uint256 takerAssetFillAmount,
+ bytes[] memory signatures
+ )
+ public
+ nonReentrant
+ returns (FillResults memory totalFillResults)
+ {
+ bytes memory takerAssetData = orders[0].takerAssetData;
+
+ uint256 ordersLength = orders.length;
+ for (uint256 i = 0; i != ordersLength; i++) {
+
+ // We assume that asset being sold by taker is the same for each order.
+ // Rather than passing this in as calldata, we use the takerAssetData from the first order in all later orders.
+ orders[i].takerAssetData = takerAssetData;
+
+ // Calculate the remaining amount of takerAsset to sell
+ uint256 remainingTakerAssetFillAmount = safeSub(takerAssetFillAmount, totalFillResults.takerAssetFilledAmount);
+
+ // Attempt to sell the remaining amount of takerAsset
+ FillResults memory singleFillResults = fillOrderInternal(
+ orders[i],
+ remainingTakerAssetFillAmount,
+ signatures[i]
+ );
+
+ // Update amounts filled and fees paid by maker and taker
+ addFillResults(totalFillResults, singleFillResults);
+
+ // Stop execution if the entire amount of takerAsset has been sold
+ if (totalFillResults.takerAssetFilledAmount >= takerAssetFillAmount) {
+ break;
+ }
+ }
+ return totalFillResults;
+ }
+
+ /// @dev Synchronously executes multiple calls of fillOrder until total amount of takerAsset is sold by taker.
+ /// Returns false if the transaction would otherwise revert.
+ /// @param orders Array of order specifications.
+ /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
+ /// @param signatures Proofs that orders have been signed by makers.
+ /// @return Amounts filled and fees paid by makers and taker.
+ function marketSellOrdersNoThrow(
+ LibOrder.Order[] memory orders,
+ uint256 takerAssetFillAmount,
+ bytes[] memory signatures
+ )
+ public
+ returns (FillResults memory totalFillResults)
+ {
+ bytes memory takerAssetData = orders[0].takerAssetData;
+
+ uint256 ordersLength = orders.length;
+ for (uint256 i = 0; i != ordersLength; i++) {
+
+ // We assume that asset being sold by taker is the same for each order.
+ // Rather than passing this in as calldata, we use the takerAssetData from the first order in all later orders.
+ orders[i].takerAssetData = takerAssetData;
+
+ // Calculate the remaining amount of takerAsset to sell
+ uint256 remainingTakerAssetFillAmount = safeSub(takerAssetFillAmount, totalFillResults.takerAssetFilledAmount);
+
+ // Attempt to sell the remaining amount of takerAsset
+ FillResults memory singleFillResults = fillOrderNoThrow(
+ orders[i],
+ remainingTakerAssetFillAmount,
+ signatures[i]
+ );
+
+ // Update amounts filled and fees paid by maker and taker
+ addFillResults(totalFillResults, singleFillResults);
+
+ // Stop execution if the entire amount of takerAsset has been sold
+ if (totalFillResults.takerAssetFilledAmount >= takerAssetFillAmount) {
+ break;
+ }
+ }
+ return totalFillResults;
+ }
+
+ /// @dev Synchronously executes multiple calls of fillOrder until total amount of makerAsset is bought by taker.
+ /// @param orders Array of order specifications.
+ /// @param makerAssetFillAmount Desired amount of makerAsset to buy.
+ /// @param signatures Proofs that orders have been signed by makers.
+ /// @return Amounts filled and fees paid by makers and taker.
+ function marketBuyOrders(
+ LibOrder.Order[] memory orders,
+ uint256 makerAssetFillAmount,
+ bytes[] memory signatures
+ )
+ public
+ nonReentrant
+ returns (FillResults memory totalFillResults)
+ {
+ bytes memory makerAssetData = orders[0].makerAssetData;
+
+ uint256 ordersLength = orders.length;
+ for (uint256 i = 0; i != ordersLength; i++) {
+
+ // We assume that asset being bought by taker is the same for each order.
+ // Rather than passing this in as calldata, we copy the makerAssetData from the first order onto all later orders.
+ orders[i].makerAssetData = makerAssetData;
+
+ // Calculate the remaining amount of makerAsset to buy
+ uint256 remainingMakerAssetFillAmount = safeSub(makerAssetFillAmount, totalFillResults.makerAssetFilledAmount);
+
+ // Convert the remaining amount of makerAsset to buy into remaining amount
+ // of takerAsset to sell, assuming entire amount can be sold in the current order
+ uint256 remainingTakerAssetFillAmount = getPartialAmountFloor(
+ orders[i].takerAssetAmount,
+ orders[i].makerAssetAmount,
+ remainingMakerAssetFillAmount
+ );
+
+ // Attempt to sell the remaining amount of takerAsset
+ FillResults memory singleFillResults = fillOrderInternal(
+ orders[i],
+ remainingTakerAssetFillAmount,
+ signatures[i]
+ );
+
+ // Update amounts filled and fees paid by maker and taker
+ addFillResults(totalFillResults, singleFillResults);
+
+ // Stop execution if the entire amount of makerAsset has been bought
+ if (totalFillResults.makerAssetFilledAmount >= makerAssetFillAmount) {
+ break;
+ }
+ }
+ return totalFillResults;
+ }
+
+ /// @dev Synchronously executes multiple fill orders in a single transaction until total amount is bought by taker.
+ /// Returns false if the transaction would otherwise revert.
+ /// @param orders Array of order specifications.
+ /// @param makerAssetFillAmount Desired amount of makerAsset to buy.
+ /// @param signatures Proofs that orders have been signed by makers.
+ /// @return Amounts filled and fees paid by makers and taker.
+ function marketBuyOrdersNoThrow(
+ LibOrder.Order[] memory orders,
+ uint256 makerAssetFillAmount,
+ bytes[] memory signatures
+ )
+ public
+ returns (FillResults memory totalFillResults)
+ {
+ bytes memory makerAssetData = orders[0].makerAssetData;
+
+ uint256 ordersLength = orders.length;
+ for (uint256 i = 0; i != ordersLength; i++) {
+
+ // We assume that asset being bought by taker is the same for each order.
+ // Rather than passing this in as calldata, we copy the makerAssetData from the first order onto all later orders.
+ orders[i].makerAssetData = makerAssetData;
+
+ // Calculate the remaining amount of makerAsset to buy
+ uint256 remainingMakerAssetFillAmount = safeSub(makerAssetFillAmount, totalFillResults.makerAssetFilledAmount);
+
+ // Convert the remaining amount of makerAsset to buy into remaining amount
+ // of takerAsset to sell, assuming entire amount can be sold in the current order
+ uint256 remainingTakerAssetFillAmount = getPartialAmountFloor(
+ orders[i].takerAssetAmount,
+ orders[i].makerAssetAmount,
+ remainingMakerAssetFillAmount
+ );
+
+ // Attempt to sell the remaining amount of takerAsset
+ FillResults memory singleFillResults = fillOrderNoThrow(
+ orders[i],
+ remainingTakerAssetFillAmount,
+ signatures[i]
+ );
+
+ // Update amounts filled and fees paid by maker and taker
+ addFillResults(totalFillResults, singleFillResults);
+
+ // Stop execution if the entire amount of makerAsset has been bought
+ if (totalFillResults.makerAssetFilledAmount >= makerAssetFillAmount) {
+ break;
+ }
+ }
+ return totalFillResults;
+ }
+
+ /// @dev Synchronously cancels multiple orders in a single transaction.
+ /// @param orders Array of order specifications.
+ function batchCancelOrders(LibOrder.Order[] memory orders)
+ public
+ nonReentrant
+ {
+ uint256 ordersLength = orders.length;
+ for (uint256 i = 0; i != ordersLength; i++) {
+ cancelOrderInternal(orders[i]);
+ }
+ }
+
+ /// @dev Fetches information for all passed in orders.
+ /// @param orders Array of order specifications.
+ /// @return Array of OrderInfo instances that correspond to each order.
+ function getOrdersInfo(LibOrder.Order[] memory orders)
+ public
+ view
+ returns (LibOrder.OrderInfo[] memory)
+ {
+ uint256 ordersLength = orders.length;
+ LibOrder.OrderInfo[] memory ordersInfo = new LibOrder.OrderInfo[](ordersLength);
+ for (uint256 i = 0; i != ordersLength; i++) {
+ ordersInfo[i] = getOrderInfo(orders[i]);
+ }
+ return ordersInfo;
+ }
+
+ /// @dev Fills the input order. Reverts if exact takerAssetFillAmount not filled.
+ /// @param order Order struct containing order specifications.
+ /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
+ /// @param signature Proof that order has been created by maker.
+ function fillOrKillOrderInternal(
+ LibOrder.Order memory order,
+ uint256 takerAssetFillAmount,
+ bytes memory signature
+ )
+ internal
+ returns (FillResults memory fillResults)
+ {
+ fillResults = fillOrderInternal(
+ order,
+ takerAssetFillAmount,
+ signature
+ );
+ require(
+ fillResults.takerAssetFilledAmount == takerAssetFillAmount,
+ "COMPLETE_FILL_FAILED"
+ );
+ return fillResults;
+ }
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol b/contracts/protocol/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol
new file mode 100644
index 000000000..fe3d03326
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol
@@ -0,0 +1,45 @@
+/*
+
+ 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;
+
+import "@0x/contracts-interfaces/contracts/protocol/Exchange/IAssetProxyDispatcher.sol";
+
+
+contract MAssetProxyDispatcher is
+ IAssetProxyDispatcher
+{
+ // Logs registration of new asset proxy
+ event AssetProxyRegistered(
+ bytes4 id, // Id of new registered AssetProxy.
+ address assetProxy // Address of new registered AssetProxy.
+ );
+
+ /// @dev Forwards arguments to assetProxy and calls `transferFrom`. Either succeeds or throws.
+ /// @param assetData Byte array encoded for the asset.
+ /// @param from Address to transfer token from.
+ /// @param to Address to transfer token to.
+ /// @param amount Amount of token to transfer.
+ function dispatchTransferFrom(
+ bytes memory assetData,
+ address from,
+ address to,
+ uint256 amount
+ )
+ internal;
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/mixins/MExchangeCore.sol b/contracts/protocol/contracts/protocol/Exchange/mixins/MExchangeCore.sol
new file mode 100644
index 000000000..215284900
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/mixins/MExchangeCore.sol
@@ -0,0 +1,157 @@
+/*
+
+ 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;
+pragma experimental ABIEncoderV2;
+
+import "@0x/contracts-libs/contracts/libs/LibOrder.sol";
+import "@0x/contracts-libs/contracts/libs/LibFillResults.sol";
+import "@0x/contracts-interfaces/contracts/protocol/Exchange/IExchangeCore.sol";
+
+
+contract MExchangeCore is
+ IExchangeCore
+{
+ // Fill event is emitted whenever an order is filled.
+ event Fill(
+ address indexed makerAddress, // Address that created the order.
+ address indexed feeRecipientAddress, // Address that received fees.
+ address takerAddress, // Address that filled the order.
+ address senderAddress, // Address that called the Exchange contract (msg.sender).
+ uint256 makerAssetFilledAmount, // Amount of makerAsset sold by maker and bought by taker.
+ uint256 takerAssetFilledAmount, // Amount of takerAsset sold by taker and bought by maker.
+ uint256 makerFeePaid, // Amount of ZRX paid to feeRecipient by maker.
+ uint256 takerFeePaid, // Amount of ZRX paid to feeRecipient by taker.
+ bytes32 indexed orderHash, // EIP712 hash of order (see LibOrder.getOrderHash).
+ bytes makerAssetData, // Encoded data specific to makerAsset.
+ bytes takerAssetData // Encoded data specific to takerAsset.
+ );
+
+ // Cancel event is emitted whenever an individual order is cancelled.
+ event Cancel(
+ address indexed makerAddress, // Address that created the order.
+ address indexed feeRecipientAddress, // Address that would have recieved fees if order was filled.
+ address senderAddress, // Address that called the Exchange contract (msg.sender).
+ bytes32 indexed orderHash, // EIP712 hash of order (see LibOrder.getOrderHash).
+ bytes makerAssetData, // Encoded data specific to makerAsset.
+ bytes takerAssetData // Encoded data specific to takerAsset.
+ );
+
+ // CancelUpTo event is emitted whenever `cancelOrdersUpTo` is executed succesfully.
+ event CancelUpTo(
+ address indexed makerAddress, // Orders cancelled must have been created by this address.
+ address indexed senderAddress, // Orders cancelled must have a `senderAddress` equal to this address.
+ uint256 orderEpoch // Orders with specified makerAddress and senderAddress with a salt less than this value are considered cancelled.
+ );
+
+ /// @dev Fills the input order.
+ /// @param order Order struct containing order specifications.
+ /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
+ /// @param signature Proof that order has been created by maker.
+ /// @return Amounts filled and fees paid by maker and taker.
+ function fillOrderInternal(
+ LibOrder.Order memory order,
+ uint256 takerAssetFillAmount,
+ bytes memory signature
+ )
+ internal
+ returns (LibFillResults.FillResults memory fillResults);
+
+ /// @dev After calling, the order can not be filled anymore.
+ /// @param order Order struct containing order specifications.
+ function cancelOrderInternal(LibOrder.Order memory order)
+ internal;
+
+ /// @dev Updates state with results of a fill order.
+ /// @param order that was filled.
+ /// @param takerAddress Address of taker who filled the order.
+ /// @param orderTakerAssetFilledAmount Amount of order already filled.
+ /// @return fillResults Amounts filled and fees paid by maker and taker.
+ function updateFilledState(
+ LibOrder.Order memory order,
+ address takerAddress,
+ bytes32 orderHash,
+ uint256 orderTakerAssetFilledAmount,
+ LibFillResults.FillResults memory fillResults
+ )
+ internal;
+
+ /// @dev Updates state with results of cancelling an order.
+ /// State is only updated if the order is currently fillable.
+ /// Otherwise, updating state would have no effect.
+ /// @param order that was cancelled.
+ /// @param orderHash Hash of order that was cancelled.
+ function updateCancelledState(
+ LibOrder.Order memory order,
+ bytes32 orderHash
+ )
+ internal;
+
+ /// @dev Validates context for fillOrder. Succeeds or throws.
+ /// @param order to be filled.
+ /// @param orderInfo OrderStatus, orderHash, and amount already filled of order.
+ /// @param takerAddress Address of order taker.
+ /// @param signature Proof that the orders was created by its maker.
+ function assertFillableOrder(
+ LibOrder.Order memory order,
+ LibOrder.OrderInfo memory orderInfo,
+ address takerAddress,
+ bytes memory signature
+ )
+ internal
+ view;
+
+ /// @dev Validates context for fillOrder. Succeeds or throws.
+ /// @param order to be filled.
+ /// @param orderInfo Status, orderHash, and amount already filled of order.
+ /// @param takerAssetFillAmount Desired amount of order to fill by taker.
+ /// @param takerAssetFilledAmount Amount of takerAsset that will be filled.
+ /// @param makerAssetFilledAmount Amount of makerAsset that will be transfered.
+ function assertValidFill(
+ LibOrder.Order memory order,
+ LibOrder.OrderInfo memory orderInfo,
+ uint256 takerAssetFillAmount,
+ uint256 takerAssetFilledAmount,
+ uint256 makerAssetFilledAmount
+ )
+ internal
+ view;
+
+ /// @dev Validates context for cancelOrder. Succeeds or throws.
+ /// @param order to be cancelled.
+ /// @param orderInfo OrderStatus, orderHash, and amount already filled of order.
+ function assertValidCancel(
+ LibOrder.Order memory order,
+ LibOrder.OrderInfo memory orderInfo
+ )
+ internal
+ view;
+
+ /// @dev Calculates amounts filled and fees paid by maker and taker.
+ /// @param order to be filled.
+ /// @param takerAssetFilledAmount Amount of takerAsset that will be filled.
+ /// @return fillResults Amounts filled and fees paid by maker and taker.
+ function calculateFillResults(
+ LibOrder.Order memory order,
+ uint256 takerAssetFilledAmount
+ )
+ internal
+ pure
+ returns (LibFillResults.FillResults memory fillResults);
+
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/mixins/MMatchOrders.sol b/contracts/protocol/contracts/protocol/Exchange/mixins/MMatchOrders.sol
new file mode 100644
index 000000000..1eb4be329
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/mixins/MMatchOrders.sol
@@ -0,0 +1,58 @@
+/*
+
+ 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;
+pragma experimental ABIEncoderV2;
+
+import "@0x/contracts-libs/contracts/libs/LibOrder.sol";
+import "@0x/contracts-libs/contracts/libs/LibFillResults.sol";
+import "@0x/contracts-interfaces/contracts/protocol/Exchange/IMatchOrders.sol";
+
+
+contract MMatchOrders is
+ IMatchOrders
+{
+ /// @dev Validates context for matchOrders. Succeeds or throws.
+ /// @param leftOrder First order to match.
+ /// @param rightOrder Second order to match.
+ function assertValidMatch(
+ LibOrder.Order memory leftOrder,
+ LibOrder.Order memory rightOrder
+ )
+ internal
+ pure;
+
+ /// @dev Calculates fill amounts for the matched orders.
+ /// Each order is filled at their respective price point. However, the calculations are
+ /// carried out as though the orders are both being filled at the right order's price point.
+ /// The profit made by the leftOrder order goes to the taker (who matched the two orders).
+ /// @param leftOrder First order to match.
+ /// @param rightOrder Second order to match.
+ /// @param leftOrderTakerAssetFilledAmount Amount of left order already filled.
+ /// @param rightOrderTakerAssetFilledAmount Amount of right order already filled.
+ /// @param matchedFillResults Amounts to fill and fees to pay by maker and taker of matched orders.
+ function calculateMatchedFillResults(
+ LibOrder.Order memory leftOrder,
+ LibOrder.Order memory rightOrder,
+ uint256 leftOrderTakerAssetFilledAmount,
+ uint256 rightOrderTakerAssetFilledAmount
+ )
+ internal
+ pure
+ returns (LibFillResults.MatchedFillResults memory matchedFillResults);
+
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/mixins/MSignatureValidator.sol b/contracts/protocol/contracts/protocol/Exchange/mixins/MSignatureValidator.sol
new file mode 100644
index 000000000..a933976d1
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/mixins/MSignatureValidator.sol
@@ -0,0 +1,75 @@
+/*
+
+ 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;
+
+import "@0x/contracts-interfaces/contracts/protocol/Exchange/ISignatureValidator.sol";
+
+
+contract MSignatureValidator is
+ ISignatureValidator
+{
+ event SignatureValidatorApproval(
+ address indexed signerAddress, // Address that approves or disapproves a contract to verify signatures.
+ address indexed validatorAddress, // Address of signature validator contract.
+ bool approved // Approval or disapproval of validator contract.
+ );
+
+ // Allowed signature types.
+ enum SignatureType {
+ Illegal, // 0x00, default value
+ Invalid, // 0x01
+ EIP712, // 0x02
+ EthSign, // 0x03
+ Wallet, // 0x04
+ Validator, // 0x05
+ PreSigned, // 0x06
+ NSignatureTypes // 0x07, number of signature types. Always leave at end.
+ }
+
+ /// @dev Verifies signature using logic defined by Wallet contract.
+ /// @param hash Any 32 byte hash.
+ /// @param walletAddress Address that should have signed the given hash
+ /// and defines its own signature verification method.
+ /// @param signature Proof that the hash has been signed by signer.
+ /// @return True if the address recovered from the provided signature matches the input signer address.
+ function isValidWalletSignature(
+ bytes32 hash,
+ address walletAddress,
+ bytes signature
+ )
+ internal
+ view
+ returns (bool isValid);
+
+ /// @dev Verifies signature using logic defined by Validator contract.
+ /// @param validatorAddress Address of validator contract.
+ /// @param hash Any 32 byte hash.
+ /// @param signerAddress Address that should have signed the given hash.
+ /// @param signature Proof that the hash has been signed by signer.
+ /// @return True if the address recovered from the provided signature matches the input signer address.
+ function isValidValidatorSignature(
+ address validatorAddress,
+ bytes32 hash,
+ address signerAddress,
+ bytes signature
+ )
+ internal
+ view
+ returns (bool isValid);
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/mixins/MTransactions.sol b/contracts/protocol/contracts/protocol/Exchange/mixins/MTransactions.sol
new file mode 100644
index 000000000..a6b0fdc85
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/mixins/MTransactions.sol
@@ -0,0 +1,58 @@
+/*
+
+ 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;
+
+import "@0x/contracts-interfaces/contracts/protocol/Exchange/ITransactions.sol";
+
+
+contract MTransactions is
+ ITransactions
+{
+ // Hash for the EIP712 ZeroEx Transaction Schema
+ bytes32 constant internal EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH = keccak256(abi.encodePacked(
+ "ZeroExTransaction(",
+ "uint256 salt,",
+ "address signerAddress,",
+ "bytes data",
+ ")"
+ ));
+
+ /// @dev Calculates EIP712 hash of the Transaction.
+ /// @param salt Arbitrary number to ensure uniqueness of transaction hash.
+ /// @param signerAddress Address of transaction signer.
+ /// @param data AbiV2 encoded calldata.
+ /// @return EIP712 hash of the Transaction.
+ function hashZeroExTransaction(
+ uint256 salt,
+ address signerAddress,
+ bytes memory data
+ )
+ internal
+ pure
+ returns (bytes32 result);
+
+ /// @dev The current function will be called in the context of this address (either 0x transaction signer or `msg.sender`).
+ /// If calling a fill function, this address will represent the taker.
+ /// If calling a cancel function, this address will represent the maker.
+ /// @return Signer of 0x transaction if entry point is `executeTransaction`.
+ /// `msg.sender` if entry point is any other function.
+ function getCurrentContextAddress()
+ internal
+ view
+ returns (address);
+}
diff --git a/contracts/protocol/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol b/contracts/protocol/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol
new file mode 100644
index 000000000..101e7cb82
--- /dev/null
+++ b/contracts/protocol/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol
@@ -0,0 +1,41 @@
+/*
+
+ 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;
+pragma experimental ABIEncoderV2;
+
+import "@0x/contracts-libs/contracts/libs/LibOrder.sol";
+import "@0x/contracts-libs/contracts/libs/LibFillResults.sol";
+import "@0x/contracts-interfaces/contracts/protocol/Exchange/IWrapperFunctions.sol";
+
+
+contract MWrapperFunctions is
+ IWrapperFunctions
+{
+ /// @dev Fills the input order. Reverts if exact takerAssetFillAmount not filled.
+ /// @param order LibOrder.Order struct containing order specifications.
+ /// @param takerAssetFillAmount Desired amount of takerAsset to sell.
+ /// @param signature Proof that order has been created by maker.
+ function fillOrKillOrderInternal(
+ LibOrder.Order memory order,
+ uint256 takerAssetFillAmount,
+ bytes memory signature
+ )
+ internal
+ returns (LibFillResults.FillResults memory fillResults);
+}
diff --git a/contracts/protocol/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol b/contracts/protocol/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol
new file mode 100644
index 000000000..ad71fc9a1
--- /dev/null
+++ b/contracts/protocol/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol
@@ -0,0 +1,37 @@
+/*
+
+ 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;
+
+import "../../protocol/Exchange/MixinAssetProxyDispatcher.sol";
+
+
+contract TestAssetProxyDispatcher is
+ MixinAssetProxyDispatcher
+{
+ function publicDispatchTransferFrom(
+ bytes memory assetData,
+ address from,
+ address to,
+ uint256 amount
+ )
+ public
+ {
+ dispatchTransferFrom(assetData, from, to, amount);
+ }
+}
diff --git a/contracts/protocol/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol b/contracts/protocol/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol
new file mode 100644
index 000000000..52c66cb56
--- /dev/null
+++ b/contracts/protocol/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol
@@ -0,0 +1,58 @@
+/*
+
+ 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;
+
+import "../../protocol/AssetProxyOwner/AssetProxyOwner.sol";
+
+
+// solhint-disable no-empty-blocks
+contract TestAssetProxyOwner is
+ AssetProxyOwner
+{
+ constructor (
+ address[] memory _owners,
+ address[] memory _assetProxyContracts,
+ uint256 _required,
+ uint256 _secondsTimeLocked
+ )
+ public
+ AssetProxyOwner(_owners, _assetProxyContracts, _required, _secondsTimeLocked)
+ {}
+
+ function testValidRemoveAuthorizedAddressAtIndexTx(uint256 id)
+ public
+ view
+ validRemoveAuthorizedAddressAtIndexTx(id)
+ returns (bool)
+ {
+ // Do nothing. We expect reverts through the modifier
+ return true;
+ }
+
+ /// @dev Compares first 4 bytes of byte array to `removeAuthorizedAddressAtIndex` function selector.
+ /// @param data Transaction data.
+ /// @return Successful if data is a call to `removeAuthorizedAddressAtIndex`.
+ function isFunctionRemoveAuthorizedAddressAtIndex(bytes memory data)
+ public
+ pure
+ returns (bool)
+ {
+ return data.readBytes4(0) == REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR;
+ }
+}
diff --git a/contracts/protocol/contracts/test/TestExchangeInternals/TestExchangeInternals.sol b/contracts/protocol/contracts/test/TestExchangeInternals/TestExchangeInternals.sol
new file mode 100644
index 000000000..27187f8f8
--- /dev/null
+++ b/contracts/protocol/contracts/test/TestExchangeInternals/TestExchangeInternals.sol
@@ -0,0 +1,191 @@
+/*
+
+ 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;
+pragma experimental ABIEncoderV2;
+
+import "../../protocol/Exchange/Exchange.sol";
+
+
+// solhint-disable no-empty-blocks
+contract TestExchangeInternals is
+ Exchange
+{
+ constructor ()
+ public
+ Exchange("")
+ {}
+
+ /// @dev Adds properties of both FillResults instances.
+ /// Modifies the first FillResults instance specified.
+ /// Note that this function has been modified from the original
+ // internal version to return the FillResults.
+ /// @param totalFillResults Fill results instance that will be added onto.
+ /// @param singleFillResults Fill results instance that will be added to totalFillResults.
+ /// @return newTotalFillResults The result of adding singleFillResults to totalFilResults.
+ function publicAddFillResults(FillResults memory totalFillResults, FillResults memory singleFillResults)
+ public
+ pure
+ returns (FillResults memory)
+ {
+ addFillResults(totalFillResults, singleFillResults);
+ return totalFillResults;
+ }
+
+ /// @dev Calculates amounts filled and fees paid by maker and taker.
+ /// @param order to be filled.
+ /// @param takerAssetFilledAmount Amount of takerAsset that will be filled.
+ /// @return fillResults Amounts filled and fees paid by maker and taker.
+ function publicCalculateFillResults(
+ Order memory order,
+ uint256 takerAssetFilledAmount
+ )
+ public
+ pure
+ returns (FillResults memory fillResults)
+ {
+ return calculateFillResults(order, takerAssetFilledAmount);
+ }
+
+ /// @dev Calculates partial value given a numerator and denominator.
+ /// Reverts if rounding error is >= 0.1%
+ /// @param numerator Numerator.
+ /// @param denominator Denominator.
+ /// @param target Value to calculate partial of.
+ /// @return Partial value of target.
+ function publicSafeGetPartialAmountFloor(
+ uint256 numerator,
+ uint256 denominator,
+ uint256 target
+ )
+ public
+ pure
+ returns (uint256 partialAmount)
+ {
+ return safeGetPartialAmountFloor(numerator, denominator, target);
+ }
+
+ /// @dev Calculates partial value given a numerator and denominator.
+ /// Reverts if rounding error is >= 0.1%
+ /// @param numerator Numerator.
+ /// @param denominator Denominator.
+ /// @param target Value to calculate partial of.
+ /// @return Partial value of target.
+ function publicSafeGetPartialAmountCeil(
+ uint256 numerator,
+ uint256 denominator,
+ uint256 target
+ )
+ public
+ pure
+ returns (uint256 partialAmount)
+ {
+ return safeGetPartialAmountCeil(numerator, denominator, target);
+ }
+
+ /// @dev Calculates partial value given a numerator and denominator.
+ /// @param numerator Numerator.
+ /// @param denominator Denominator.
+ /// @param target Value to calculate partial of.
+ /// @return Partial value of target.
+ function publicGetPartialAmountFloor(
+ uint256 numerator,
+ uint256 denominator,
+ uint256 target
+ )
+ public
+ pure
+ returns (uint256 partialAmount)
+ {
+ return getPartialAmountFloor(numerator, denominator, target);
+ }
+
+ /// @dev Calculates partial value given a numerator and denominator.
+ /// @param numerator Numerator.
+ /// @param denominator Denominator.
+ /// @param target Value to calculate partial of.
+ /// @return Partial value of target.
+ function publicGetPartialAmountCeil(
+ uint256 numerator,
+ uint256 denominator,
+ uint256 target
+ )
+ public
+ pure
+ returns (uint256 partialAmount)
+ {
+ return getPartialAmountCeil(numerator, denominator, target);
+ }
+
+ /// @dev Checks if rounding error >= 0.1%.
+ /// @param numerator Numerator.
+ /// @param denominator Denominator.
+ /// @param target Value to multiply with numerator/denominator.
+ /// @return Rounding error is present.
+ function publicIsRoundingErrorFloor(
+ uint256 numerator,
+ uint256 denominator,
+ uint256 target
+ )
+ public
+ pure
+ returns (bool isError)
+ {
+ return isRoundingErrorFloor(numerator, denominator, target);
+ }
+
+ /// @dev Checks if rounding error >= 0.1%.
+ /// @param numerator Numerator.
+ /// @param denominator Denominator.
+ /// @param target Value to multiply with numerator/denominator.
+ /// @return Rounding error is present.
+ function publicIsRoundingErrorCeil(
+ uint256 numerator,
+ uint256 denominator,
+ uint256 target
+ )
+ public
+ pure
+ returns (bool isError)
+ {
+ return isRoundingErrorCeil(numerator, denominator, target);
+ }
+
+ /// @dev Updates state with results of a fill order.
+ /// @param order that was filled.
+ /// @param takerAddress Address of taker who filled the order.
+ /// @param orderTakerAssetFilledAmount Amount of order already filled.
+ /// @return fillResults Amounts filled and fees paid by maker and taker.
+ function publicUpdateFilledState(
+ Order memory order,
+ address takerAddress,
+ bytes32 orderHash,
+ uint256 orderTakerAssetFilledAmount,
+ FillResults memory fillResults
+ )
+ public
+ {
+ updateFilledState(
+ order,
+ takerAddress,
+ orderHash,
+ orderTakerAssetFilledAmount,
+ fillResults
+ );
+ }
+}
diff --git a/contracts/protocol/contracts/test/TestSignatureValidator/TestSignatureValidator.sol b/contracts/protocol/contracts/test/TestSignatureValidator/TestSignatureValidator.sol
new file mode 100644
index 000000000..ea3e2de59
--- /dev/null
+++ b/contracts/protocol/contracts/test/TestSignatureValidator/TestSignatureValidator.sol
@@ -0,0 +1,45 @@
+/*
+
+ 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;
+
+import "../../protocol/Exchange/MixinSignatureValidator.sol";
+import "../../protocol/Exchange/MixinTransactions.sol";
+
+
+contract TestSignatureValidator is
+ MixinSignatureValidator,
+ MixinTransactions
+{
+ function publicIsValidSignature(
+ bytes32 hash,
+ address signer,
+ bytes memory signature
+ )
+ public
+ view
+ returns (bool isValid)
+ {
+ isValid = isValidSignature(
+ hash,
+ signer,
+ signature
+ );
+ return isValid;
+ }
+}
diff --git a/contracts/protocol/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol b/contracts/protocol/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol
new file mode 100644
index 000000000..d08da7303
--- /dev/null
+++ b/contracts/protocol/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol
@@ -0,0 +1,81 @@
+/*
+
+ 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;
+
+import "@0x/contracts-tokens/contracts/tokens/ERC20Token/IERC20Token.sol";
+
+
+// solhint-disable no-unused-vars
+contract TestStaticCallReceiver {
+
+ uint256 internal state = 1;
+
+ /// @dev Updates state and returns true. Intended to be used with `Validator` signature type.
+ /// @param hash Message hash that is signed.
+ /// @param signerAddress Address that should have signed the given hash.
+ /// @param signature Proof of signing.
+ /// @return Validity of order signature.
+ function isValidSignature(
+ bytes32 hash,
+ address signerAddress,
+ bytes signature
+ )
+ external
+ returns (bool isValid)
+ {
+ updateState();
+ return true;
+ }
+
+ /// @dev Updates state and returns true. Intended to be used with `Wallet` signature type.
+ /// @param hash Message hash that is signed.
+ /// @param signature Proof of signing.
+ /// @return Validity of order signature.
+ function isValidSignature(
+ bytes32 hash,
+ bytes signature
+ )
+ external
+ returns (bool isValid)
+ {
+ updateState();
+ return true;
+ }
+
+ /// @dev Approves an ERC20 token to spend tokens from this address.
+ /// @param token Address of ERC20 token.
+ /// @param spender Address that will spend tokens.
+ /// @param value Amount of tokens spender is approved to spend.
+ function approveERC20(
+ address token,
+ address spender,
+ uint256 value
+ )
+ external
+ {
+ IERC20Token(token).approve(spender, value);
+ }
+
+ /// @dev Increments state variable.
+ function updateState()
+ internal
+ {
+ state++;
+ }
+}
diff --git a/contracts/protocol/package.json b/contracts/protocol/package.json
new file mode 100644
index 000000000..936d1bf5b
--- /dev/null
+++ b/contracts/protocol/package.json
@@ -0,0 +1,96 @@
+{
+ "private": true,
+ "name": "@0x/contracts-protocol",
+ "version": "2.1.56",
+ "engines": {
+ "node": ">=6.12"
+ },
+ "description": "Smart contract components of 0x protocol",
+ "main": "lib/src/index.js",
+ "directories": {
+ "test": "test"
+ },
+ "scripts": {
+ "build": "yarn pre_build && tsc -b",
+ "build:ci": "yarn build",
+ "pre_build": "run-s compile generate_contract_wrappers",
+ "test": "yarn run_mocha",
+ "rebuild_and_test": "run-s build test",
+ "test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov",
+ "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html",
+ "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha",
+ "run_mocha":
+ "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit",
+ "compile": "sol-compiler --contracts-dir contracts",
+ "clean": "shx rm -rf lib generated-artifacts generated-wrappers",
+ "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers",
+ "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts",
+ "coverage:report:text": "istanbul report text",
+ "coverage:report:html": "istanbul report html && open coverage/index.html",
+ "profiler:report:html": "istanbul report html && open coverage/index.html",
+ "coverage:report:lcov": "istanbul report lcov",
+ "test:circleci": "yarn test",
+ "lint-contracts": "solhint contracts/**/**/**/**/*.sol"
+ },
+ "config": {
+ "abis": "generated-artifacts/@(AssetProxyOwner|ERC20Proxy|ERC721Proxy|Exchange|MixinAuthorizable|MultiAssetProxy|TestSignatureValidator|TestAssetProxyOwner|TestAssetProxyDispatcher|TestExchangeInternals|TestStaticCallReceiver).json"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/0xProject/0x-monorepo.git"
+ },
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/0xProject/0x-monorepo/issues"
+ },
+ "homepage": "https://github.com/0xProject/0x-monorepo/contracts/protocol/README.md",
+ "devDependencies": {
+ "@0x/contracts-test-utils": "^1.0.0",
+ "@0x/abi-gen": "^1.0.17",
+ "@0x/dev-utils": "^1.0.19",
+ "@0x/sol-compiler": "^1.1.14",
+ "@0x/sol-cov": "^2.1.14",
+ "@0x/subproviders": "^2.1.6",
+ "@0x/tslint-config": "^1.0.10",
+ "@types/bn.js": "^4.11.0",
+ "@types/lodash": "4.14.104",
+ "@types/node": "*",
+ "@types/yargs": "^10.0.0",
+ "chai": "^4.0.1",
+ "chai-as-promised": "^7.1.0",
+ "chai-bignumber": "^2.0.1",
+ "dirty-chai": "^2.0.1",
+ "make-promises-safe": "^1.1.0",
+ "ethereumjs-abi": "0.6.5",
+ "mocha": "^4.1.0",
+ "npm-run-all": "^4.1.2",
+ "shx": "^0.2.2",
+ "solc": "^0.4.24",
+ "solhint": "^1.2.1",
+ "tslint": "5.11.0",
+ "typescript": "3.0.1",
+ "yargs": "^10.0.3"
+ },
+ "dependencies": {
+ "@0x/base-contract": "^3.0.8",
+ "@0x/order-utils": "^3.0.4",
+ "@0x/contracts-multisig": "^1.0.0",
+ "@0x/contracts-utils": "^1.0.0",
+ "@0x/contracts-tokens": "^1.0.0",
+ "@0x/contracts-examples": "^1.0.0",
+ "@0x/contracts-libs": "^1.0.0",
+ "@0x/contracts-interfaces": "^1.0.0",
+ "@0x/types": "^1.3.0",
+ "@0x/typescript-typings": "^3.0.4",
+ "@0x/utils": "^2.0.6",
+ "@0x/web3-wrapper": "^3.1.6",
+ "@types/js-combinatorics": "^0.5.29",
+ "bn.js": "^4.11.8",
+ "ethereum-types": "^1.1.2",
+ "ethereumjs-util": "^5.1.1",
+ "lodash": "^4.17.5"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/contracts/protocol/src/artifacts/index.ts b/contracts/protocol/src/artifacts/index.ts
new file mode 100644
index 000000000..c5d12f10b
--- /dev/null
+++ b/contracts/protocol/src/artifacts/index.ts
@@ -0,0 +1,27 @@
+import { ContractArtifact } from 'ethereum-types';
+
+import * as AssetProxyOwner from '../../generated-artifacts/AssetProxyOwner.json';
+import * as ERC20Proxy from '../../generated-artifacts/ERC20Proxy.json';
+import * as ERC721Proxy from '../../generated-artifacts/ERC721Proxy.json';
+import * as Exchange from '../../generated-artifacts/Exchange.json';
+import * as MixinAuthorizable from '../../generated-artifacts/MixinAuthorizable.json';
+import * as MultiAssetProxy from '../../generated-artifacts/MultiAssetProxy.json';
+import * as TestAssetProxyDispatcher from '../../generated-artifacts/TestAssetProxyDispatcher.json';
+import * as TestAssetProxyOwner from '../../generated-artifacts/TestAssetProxyOwner.json';
+import * as TestExchangeInternals from '../../generated-artifacts/TestExchangeInternals.json';
+import * as TestSignatureValidator from '../../generated-artifacts/TestSignatureValidator.json';
+import * as TestStaticCallReceiver from '../../generated-artifacts/TestStaticCallReceiver.json';
+
+export const artifacts = {
+ AssetProxyOwner: AssetProxyOwner as ContractArtifact,
+ ERC20Proxy: ERC20Proxy as ContractArtifact,
+ ERC721Proxy: ERC721Proxy as ContractArtifact,
+ Exchange: Exchange as ContractArtifact,
+ MixinAuthorizable: MixinAuthorizable as ContractArtifact,
+ MultiAssetProxy: MultiAssetProxy as ContractArtifact,
+ TestAssetProxyDispatcher: TestAssetProxyDispatcher as ContractArtifact,
+ TestAssetProxyOwner: TestAssetProxyOwner as ContractArtifact,
+ TestExchangeInternals: TestExchangeInternals as ContractArtifact,
+ TestSignatureValidator: TestSignatureValidator as ContractArtifact,
+ TestStaticCallReceiver: TestStaticCallReceiver as ContractArtifact,
+};
diff --git a/contracts/protocol/src/index.ts b/contracts/protocol/src/index.ts
new file mode 100644
index 000000000..ba813e7ca
--- /dev/null
+++ b/contracts/protocol/src/index.ts
@@ -0,0 +1,3 @@
+export * from './artifacts';
+export * from './wrappers';
+export * from '../test/utils';
diff --git a/contracts/protocol/src/wrappers/index.ts b/contracts/protocol/src/wrappers/index.ts
new file mode 100644
index 000000000..01b121054
--- /dev/null
+++ b/contracts/protocol/src/wrappers/index.ts
@@ -0,0 +1,10 @@
+export * from '../../generated-wrappers/asset_proxy_owner';
+export * from '../../generated-wrappers/erc20_proxy';
+export * from '../../generated-wrappers/erc721_proxy';
+export * from '../../generated-wrappers/exchange';
+export * from '../../generated-wrappers/mixin_authorizable';
+export * from '../../generated-wrappers/test_asset_proxy_dispatcher';
+export * from '../../generated-wrappers/test_asset_proxy_owner';
+export * from '../../generated-wrappers/test_exchange_internals';
+export * from '../../generated-wrappers/test_signature_validator';
+export * from '../../generated-wrappers/test_static_call_receiver';
diff --git a/contracts/protocol/test/asset_proxy/authorizable.ts b/contracts/protocol/test/asset_proxy/authorizable.ts
new file mode 100644
index 000000000..853d18be0
--- /dev/null
+++ b/contracts/protocol/test/asset_proxy/authorizable.ts
@@ -0,0 +1,211 @@
+import {
+ chaiSetup,
+ constants,
+ expectTransactionFailedAsync,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { RevertReason } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { MixinAuthorizableContract } from '../../generated-wrappers/mixin_authorizable';
+import { artifacts } from '../../src/artifacts';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('Authorizable', () => {
+ let owner: string;
+ let notOwner: string;
+ let address: string;
+ let authorizable: MixinAuthorizableContract;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ [owner, address, notOwner] = _.slice(accounts, 0, 3);
+ authorizable = await MixinAuthorizableContract.deployFrom0xArtifactAsync(
+ artifacts.MixinAuthorizable,
+ provider,
+ txDefaults,
+ );
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('addAuthorizedAddress', () => {
+ it('should throw if not called by owner', async () => {
+ return expectTransactionFailedAsync(
+ authorizable.addAuthorizedAddress.sendTransactionAsync(notOwner, { from: notOwner }),
+ RevertReason.OnlyContractOwner,
+ );
+ });
+ it('should allow owner to add an authorized address', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const isAuthorized = await authorizable.authorized.callAsync(address);
+ expect(isAuthorized).to.be.true();
+ });
+ it('should throw if owner attempts to authorize a duplicate address', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ return expectTransactionFailedAsync(
+ authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ RevertReason.TargetAlreadyAuthorized,
+ );
+ });
+ });
+
+ describe('removeAuthorizedAddress', () => {
+ it('should throw if not called by owner', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddress.sendTransactionAsync(address, {
+ from: notOwner,
+ }),
+ RevertReason.OnlyContractOwner,
+ );
+ });
+
+ it('should allow owner to remove an authorized address', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.removeAuthorizedAddress.sendTransactionAsync(address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const isAuthorized = await authorizable.authorized.callAsync(address);
+ expect(isAuthorized).to.be.false();
+ });
+
+ it('should throw if owner attempts to remove an address that is not authorized', async () => {
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddress.sendTransactionAsync(address, {
+ from: owner,
+ }),
+ RevertReason.TargetNotAuthorized,
+ );
+ });
+ });
+
+ describe('removeAuthorizedAddressAtIndex', () => {
+ it('should throw if not called by owner', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const index = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
+ from: notOwner,
+ }),
+ RevertReason.OnlyContractOwner,
+ );
+ });
+ it('should throw if index is >= authorities.length', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const index = new BigNumber(1);
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
+ from: owner,
+ }),
+ RevertReason.IndexOutOfBounds,
+ );
+ });
+ it('should throw if owner attempts to remove an address that is not authorized', async () => {
+ const index = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
+ from: owner,
+ }),
+ RevertReason.TargetNotAuthorized,
+ );
+ });
+ it('should throw if address at index does not match target', async () => {
+ const address1 = address;
+ const address2 = notOwner;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address1, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address2, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const address1Index = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address2, address1Index, {
+ from: owner,
+ }),
+ RevertReason.AuthorizedAddressMismatch,
+ );
+ });
+ it('should allow owner to remove an authorized address', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const index = new BigNumber(0);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const isAuthorized = await authorizable.authorized.callAsync(address);
+ expect(isAuthorized).to.be.false();
+ });
+ });
+
+ describe('getAuthorizedAddresses', () => {
+ it('should return all authorized addresses', async () => {
+ const initial = await authorizable.getAuthorizedAddresses.callAsync();
+ expect(initial).to.have.length(0);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.addAuthorizedAddress.sendTransactionAsync(address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const afterAdd = await authorizable.getAuthorizedAddresses.callAsync();
+ expect(afterAdd).to.have.length(1);
+ expect(afterAdd).to.include(address);
+
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await authorizable.removeAuthorizedAddress.sendTransactionAsync(address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const afterRemove = await authorizable.getAuthorizedAddresses.callAsync();
+ expect(afterRemove).to.have.length(0);
+ });
+ });
+});
diff --git a/contracts/protocol/test/asset_proxy/proxies.ts b/contracts/protocol/test/asset_proxy/proxies.ts
new file mode 100644
index 000000000..c4bd95905
--- /dev/null
+++ b/contracts/protocol/test/asset_proxy/proxies.ts
@@ -0,0 +1,1253 @@
+import { artifacts as interfacesArtifacts, IAssetDataContract, IAssetProxyContract } from '@0x/contracts-interfaces';
+import {
+ chaiSetup,
+ constants,
+ expectTransactionFailedAsync,
+ expectTransactionFailedWithoutReasonAsync,
+ LogDecoder,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import {
+ artifacts as tokensArtifacts,
+ DummyERC20TokenContract,
+ DummyERC721ReceiverContract,
+ DummyERC721TokenContract,
+ DummyMultipleReturnERC20TokenContract,
+ DummyNoReturnERC20TokenContract,
+} from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils } from '@0x/order-utils';
+import { RevertReason } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import { MultiAssetProxyContract } from '../../generated-wrappers/multi_asset_proxy';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+const assetProxyInterface = new IAssetProxyContract(
+ interfacesArtifacts.IAssetProxy.compilerOutput.abi,
+ constants.NULL_ADDRESS,
+ provider,
+);
+const assetDataInterface = new IAssetDataContract(
+ interfacesArtifacts.IAssetData.compilerOutput.abi,
+ constants.NULL_ADDRESS,
+ provider,
+);
+
+// tslint:disable:no-unnecessary-type-assertion
+describe('Asset Transfer Proxies', () => {
+ let owner: string;
+ let notAuthorized: string;
+ let authorized: string;
+ let fromAddress: string;
+ let toAddress: string;
+
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc20TokenB: DummyERC20TokenContract;
+ let erc721TokenA: DummyERC721TokenContract;
+ let erc721TokenB: DummyERC721TokenContract;
+ let erc721Receiver: DummyERC721ReceiverContract;
+ let erc20Proxy: ERC20ProxyContract;
+ let erc721Proxy: ERC721ProxyContract;
+ let noReturnErc20Token: DummyNoReturnERC20TokenContract;
+ let multipleReturnErc20Token: DummyMultipleReturnERC20TokenContract;
+ let multiAssetProxy: MultiAssetProxyContract;
+
+ let erc20Wrapper: ERC20Wrapper;
+ let erc721Wrapper: ERC721Wrapper;
+ let erc721AFromTokenId: BigNumber;
+ let erc721BFromTokenId: BigNumber;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, notAuthorized, authorized, fromAddress, toAddress] = _.slice(accounts, 0, 5));
+
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+ erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+
+ // Deploy AssetProxies
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync(
+ artifacts.MultiAssetProxy,
+ provider,
+ txDefaults,
+ );
+
+ // Configure ERC20Proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(authorized, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Configure ERC721Proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(authorized, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Configure MultiAssetProxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.addAuthorizedAddress.sendTransactionAsync(authorized, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc721Proxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Deploy and configure ERC20 tokens
+ const numDummyErc20ToDeploy = 2;
+ [erc20TokenA, erc20TokenB] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ noReturnErc20Token = await DummyNoReturnERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyNoReturnERC20Token,
+ provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ constants.DUMMY_TOKEN_DECIMALS,
+ constants.DUMMY_TOKEN_TOTAL_SUPPLY,
+ );
+ multipleReturnErc20Token = await DummyMultipleReturnERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyMultipleReturnERC20Token,
+ provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ constants.DUMMY_TOKEN_DECIMALS,
+ constants.DUMMY_TOKEN_TOTAL_SUPPLY,
+ );
+
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await noReturnErc20Token.setBalance.sendTransactionAsync(fromAddress, constants.INITIAL_ERC20_BALANCE),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await noReturnErc20Token.approve.sendTransactionAsync(
+ erc20Proxy.address,
+ constants.INITIAL_ERC20_ALLOWANCE,
+ { from: fromAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multipleReturnErc20Token.setBalance.sendTransactionAsync(
+ fromAddress,
+ constants.INITIAL_ERC20_BALANCE,
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multipleReturnErc20Token.approve.sendTransactionAsync(
+ erc20Proxy.address,
+ constants.INITIAL_ERC20_ALLOWANCE,
+ { from: fromAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Deploy and configure ERC721 tokens and receiver
+ [erc721TokenA, erc721TokenB] = await erc721Wrapper.deployDummyTokensAsync();
+ erc721Receiver = await DummyERC721ReceiverContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyERC721Receiver,
+ provider,
+ txDefaults,
+ );
+
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ erc721AFromTokenId = erc721Balances[fromAddress][erc721TokenA.address][0];
+ erc721BFromTokenId = erc721Balances[fromAddress][erc721TokenB.address][0];
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+
+ describe('ERC20Proxy', () => {
+ it('should revert if undefined function is called', async () => {
+ const undefinedSelector = '0x01020304';
+ await expectTransactionFailedWithoutReasonAsync(
+ web3Wrapper.sendTransactionAsync({
+ from: owner,
+ to: erc20Proxy.address,
+ value: constants.ZERO_AMOUNT,
+ data: undefinedSelector,
+ }),
+ );
+ });
+ it('should have an id of 0xf47261b0', async () => {
+ const proxyId = await erc20Proxy.getProxyId.callAsync();
+ const expectedProxyId = '0xf47261b0';
+ expect(proxyId).to.equal(expectedProxyId);
+ });
+ describe('transferFrom', () => {
+ it('should successfully transfer tokens', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ // Perform a transfer from fromAddress to toAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(amount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(amount),
+ );
+ });
+
+ it('should successfully transfer tokens that do not return a value', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address);
+ // Perform a transfer from fromAddress to toAddress
+ const initialFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const initialToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress);
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const newToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress);
+ expect(newFromBalance).to.be.bignumber.equal(initialFromBalance.minus(amount));
+ expect(newToBalance).to.be.bignumber.equal(initialToBalance.plus(amount));
+ });
+
+ it('should successfully transfer tokens and ignore extra assetData', async () => {
+ // Construct ERC20 asset data
+ const extraData = '0102030405060708';
+ const encodedAssetData = `${assetDataUtils.encodeERC20AssetData(erc20TokenA.address)}${extraData}`;
+ // Perform a transfer from fromAddress to toAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(amount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(amount),
+ );
+ });
+
+ it('should do nothing if transferring 0 amount of a token', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ // Perform a transfer from fromAddress to toAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = new BigNumber(0);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address],
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address],
+ );
+ });
+
+ it('should revert if allowances are too low', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ // Create allowance less than transfer amount. Set allowance on proxy.
+ const allowance = new BigNumber(0);
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, allowance, {
+ from: fromAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ // Perform a transfer; expect this to fail.
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.TransferFailed,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.deep.equal(erc20Balances);
+ });
+
+ it('should revert if allowances are too low and token does not return a value', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address);
+ // Create allowance less than transfer amount. Set allowance on proxy.
+ const allowance = new BigNumber(0);
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await noReturnErc20Token.approve.sendTransactionAsync(erc20Proxy.address, allowance, {
+ from: fromAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const initialFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const initialToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress);
+ // Perform a transfer; expect this to fail.
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.TransferFailed,
+ );
+ const newFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const newToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress);
+ expect(newFromBalance).to.be.bignumber.equal(initialFromBalance);
+ expect(newToBalance).to.be.bignumber.equal(initialToBalance);
+ });
+
+ it('should revert if caller is not authorized', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: notAuthorized,
+ }),
+ RevertReason.SenderNotAuthorized,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.deep.equal(erc20Balances);
+ });
+
+ it('should revert if token returns more than 32 bytes', async () => {
+ // Construct ERC20 asset data
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(multipleReturnErc20Token.address);
+ const amount = new BigNumber(10);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ const initialFromBalance = await multipleReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const initialToBalance = await multipleReturnErc20Token.balanceOf.callAsync(toAddress);
+ // Perform a transfer; expect this to fail.
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc20Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.TransferFailed,
+ );
+ const newFromBalance = await multipleReturnErc20Token.balanceOf.callAsync(fromAddress);
+ const newToBalance = await multipleReturnErc20Token.balanceOf.callAsync(toAddress);
+ expect(newFromBalance).to.be.bignumber.equal(initialFromBalance);
+ expect(newToBalance).to.be.bignumber.equal(initialToBalance);
+ });
+ });
+ });
+
+ describe('ERC721Proxy', () => {
+ it('should revert if undefined function is called', async () => {
+ const undefinedSelector = '0x01020304';
+ await expectTransactionFailedWithoutReasonAsync(
+ web3Wrapper.sendTransactionAsync({
+ from: owner,
+ to: erc721Proxy.address,
+ value: constants.ZERO_AMOUNT,
+ data: undefinedSelector,
+ }),
+ );
+ });
+ it('should have an id of 0x02571792', async () => {
+ const proxyId = await erc721Proxy.getProxyId.callAsync();
+ const expectedProxyId = '0x02571792';
+ expect(proxyId).to.equal(expectedProxyId);
+ });
+ describe('transferFrom', () => {
+ it('should successfully transfer tokens', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(1);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.bignumber.equal(toAddress);
+ });
+
+ it('should successfully transfer tokens and ignore extra assetData', async () => {
+ // Construct ERC721 asset data
+ const extraData = '0102030405060708';
+ const encodedAssetData = `${assetDataUtils.encodeERC721AssetData(
+ erc721TokenA.address,
+ erc721AFromTokenId,
+ )}${extraData}`;
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(1);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.bignumber.equal(toAddress);
+ });
+
+ it('should not call onERC721Received when transferring to a smart contract', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(1);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ erc721Receiver.address,
+ amount,
+ );
+ const logDecoder = new LogDecoder(web3Wrapper, { ...artifacts, ...tokensArtifacts });
+ const tx = await logDecoder.getTxWithDecodedLogsAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ gas: constants.MAX_TRANSFER_FROM_GAS,
+ }),
+ );
+ // Verify that no log was emitted by erc721 receiver
+ expect(tx.logs.length).to.be.equal(1);
+ // Verify transfer was successful
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.bignumber.equal(erc721Receiver.address);
+ });
+
+ it('should revert if transferring 0 amount of a token', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(0);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.InvalidAmount,
+ );
+ const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwner).to.be.equal(ownerFromAsset);
+ });
+
+ it('should revert if transferring > 1 amount of a token', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(500);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.InvalidAmount,
+ );
+ const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwner).to.be.equal(ownerFromAsset);
+ });
+
+ it('should revert if allowances are too low', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Remove transfer approval for fromAddress.
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721TokenA.approve.sendTransactionAsync(constants.NULL_ADDRESS, erc721AFromTokenId, {
+ from: fromAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Perform a transfer; expect this to fail.
+ const amount = new BigNumber(1);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.TransferFailed,
+ );
+ const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwner).to.be.equal(ownerFromAsset);
+ });
+
+ it('should revert if caller is not authorized', async () => {
+ // Construct ERC721 asset data
+ const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ // Verify pre-condition
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ // Perform a transfer from fromAddress to toAddress
+ const amount = new BigNumber(1);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ encodedAssetData,
+ fromAddress,
+ toAddress,
+ amount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: erc721Proxy.address,
+ data,
+ from: notAuthorized,
+ }),
+ RevertReason.SenderNotAuthorized,
+ );
+ const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwner).to.be.equal(ownerFromAsset);
+ });
+ });
+ });
+ describe('MultiAssetProxy', () => {
+ it('should revert if undefined function is called', async () => {
+ const undefinedSelector = '0x01020304';
+ await expectTransactionFailedWithoutReasonAsync(
+ web3Wrapper.sendTransactionAsync({
+ from: owner,
+ to: multiAssetProxy.address,
+ value: constants.ZERO_AMOUNT,
+ data: undefinedSelector,
+ }),
+ );
+ });
+ it('should have an id of 0x94cfcdd7', async () => {
+ const proxyId = await multiAssetProxy.getProxyId.callAsync();
+ // first 4 bytes of `keccak256('MultiAsset(uint256[],bytes[])')`
+ const expectedProxyId = '0x94cfcdd7';
+ expect(proxyId).to.equal(expectedProxyId);
+ });
+ describe('transferFrom', () => {
+ it('should transfer a single ERC20 token', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const amounts = [erc20Amount];
+ const nestedAssetData = [erc20AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalAmount = inputAmount.times(erc20Amount);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalAmount),
+ );
+ });
+ it('should successfully transfer multiple of the same ERC20 token', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount1 = new BigNumber(10);
+ const erc20Amount2 = new BigNumber(20);
+ const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const amounts = [erc20Amount1, erc20Amount2];
+ const nestedAssetData = [erc20AssetData1, erc20AssetData2];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalAmount = inputAmount.times(erc20Amount1).plus(inputAmount.times(erc20Amount2));
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalAmount),
+ );
+ });
+ it('should successfully transfer multiple different ERC20 tokens', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount1 = new BigNumber(10);
+ const erc20Amount2 = new BigNumber(20);
+ const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address);
+ const amounts = [erc20Amount1, erc20Amount2];
+ const nestedAssetData = [erc20AssetData1, erc20AssetData2];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalErc20AAmount = inputAmount.times(erc20Amount1);
+ const totalErc20BAmount = inputAmount.times(erc20Amount2);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount),
+ );
+ expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount),
+ );
+ });
+ it('should transfer a single ERC721 token', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc721Amount];
+ const nestedAssetData = [erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.equal(toAddress);
+ });
+ it('should successfully transfer multiple of the same ERC721 token', async () => {
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ const erc721AFromTokenId2 = erc721Balances[fromAddress][erc721TokenA.address][1];
+ const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const erc721AssetData2 = assetDataUtils.encodeERC721AssetData(
+ erc721TokenA.address,
+ erc721AFromTokenId2,
+ );
+ const inputAmount = new BigNumber(1);
+ const erc721Amount = new BigNumber(1);
+ const amounts = [erc721Amount, erc721Amount];
+ const nestedAssetData = [erc721AssetData1, erc721AssetData2];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset1).to.be.equal(fromAddress);
+ const ownerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2);
+ expect(ownerFromAsset2).to.be.equal(fromAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ gas: constants.MAX_TRANSFER_FROM_GAS,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ const newOwnerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2);
+ expect(newOwnerFromAsset1).to.be.equal(toAddress);
+ expect(newOwnerFromAsset2).to.be.equal(toAddress);
+ });
+ it('should successfully transfer multiple different ERC721 tokens', async () => {
+ const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const erc721AssetData2 = assetDataUtils.encodeERC721AssetData(erc721TokenB.address, erc721BFromTokenId);
+ const inputAmount = new BigNumber(1);
+ const erc721Amount = new BigNumber(1);
+ const amounts = [erc721Amount, erc721Amount];
+ const nestedAssetData = [erc721AssetData1, erc721AssetData2];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset1).to.be.equal(fromAddress);
+ const ownerFromAsset2 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId);
+ expect(ownerFromAsset2).to.be.equal(fromAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ gas: constants.MAX_TRANSFER_FROM_GAS,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ const newOwnerFromAsset2 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId);
+ expect(newOwnerFromAsset1).to.be.equal(toAddress);
+ expect(newOwnerFromAsset2).to.be.equal(toAddress);
+ });
+ it('should successfully transfer a combination of ERC20 and ERC721 tokens', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalAmount = inputAmount.times(erc20Amount);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalAmount),
+ );
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.equal(toAddress);
+ });
+ it('should successfully transfer tokens and ignore extra assetData', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const extraData = '0102030405060708';
+ const assetData = `${assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ amounts,
+ nestedAssetData,
+ )}${extraData}`;
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset).to.be.equal(fromAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalAmount = inputAmount.times(erc20Amount);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalAmount),
+ );
+ const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(newOwnerFromAsset).to.be.equal(toAddress);
+ });
+ it('should successfully transfer correct amounts when the `amount` > 1', async () => {
+ const inputAmount = new BigNumber(100);
+ const erc20Amount1 = new BigNumber(10);
+ const erc20Amount2 = new BigNumber(20);
+ const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address);
+ const amounts = [erc20Amount1, erc20Amount2];
+ const nestedAssetData = [erc20AssetData1, erc20AssetData2];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalErc20AAmount = inputAmount.times(erc20Amount1);
+ const totalErc20BAmount = inputAmount.times(erc20Amount2);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount),
+ );
+ expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount),
+ );
+ });
+ it('should successfully transfer a large amount of tokens', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount1 = new BigNumber(10);
+ const erc20Amount2 = new BigNumber(20);
+ const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ const erc721AFromTokenId2 = erc721Balances[fromAddress][erc721TokenA.address][1];
+ const erc721BFromTokenId2 = erc721Balances[fromAddress][erc721TokenB.address][1];
+ const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const erc721AssetData2 = assetDataUtils.encodeERC721AssetData(
+ erc721TokenA.address,
+ erc721AFromTokenId2,
+ );
+ const erc721AssetData3 = assetDataUtils.encodeERC721AssetData(erc721TokenB.address, erc721BFromTokenId);
+ const erc721AssetData4 = assetDataUtils.encodeERC721AssetData(
+ erc721TokenB.address,
+ erc721BFromTokenId2,
+ );
+ const amounts = [erc721Amount, erc20Amount1, erc721Amount, erc20Amount2, erc721Amount, erc721Amount];
+ const nestedAssetData = [
+ erc721AssetData1,
+ erc20AssetData1,
+ erc721AssetData2,
+ erc20AssetData2,
+ erc721AssetData3,
+ erc721AssetData4,
+ ];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ expect(ownerFromAsset1).to.be.equal(fromAddress);
+ const ownerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2);
+ expect(ownerFromAsset2).to.be.equal(fromAddress);
+ const ownerFromAsset3 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId);
+ expect(ownerFromAsset3).to.be.equal(fromAddress);
+ const ownerFromAsset4 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId2);
+ expect(ownerFromAsset4).to.be.equal(fromAddress);
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ gas: constants.MAX_EXECUTE_TRANSACTION_GAS,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId);
+ const newOwnerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2);
+ const newOwnerFromAsset3 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId);
+ const newOwnerFromAsset4 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId2);
+ expect(newOwnerFromAsset1).to.be.equal(toAddress);
+ expect(newOwnerFromAsset2).to.be.equal(toAddress);
+ expect(newOwnerFromAsset3).to.be.equal(toAddress);
+ expect(newOwnerFromAsset4).to.be.equal(toAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const totalErc20AAmount = inputAmount.times(erc20Amount1);
+ const totalErc20BAmount = inputAmount.times(erc20Amount2);
+ expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount),
+ );
+ expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount),
+ );
+ expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal(
+ erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount),
+ );
+ });
+ it('should revert if a single transfer fails', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ // 2 is an invalid erc721 amount
+ const erc721Amount = new BigNumber(2);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.InvalidAmount,
+ );
+ });
+ it('should revert if an AssetProxy is not registered', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const invalidProxyId = '0x12345678';
+ const invalidErc721AssetData = `${invalidProxyId}${erc721AssetData.slice(10)}`;
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, invalidErc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.AssetProxyDoesNotExist,
+ );
+ });
+ it('should revert if the length of `amounts` does not match the length of `nestedAssetData`', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc20Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.LengthMismatch,
+ );
+ });
+ it('should revert if amounts multiplication results in an overflow', async () => {
+ const inputAmount = new BigNumber(2).pow(128);
+ const erc20Amount = new BigNumber(2).pow(128);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const amounts = [erc20Amount];
+ const nestedAssetData = [erc20AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.Uint256Overflow,
+ );
+ });
+ it('should revert if an element of `nestedAssetData` is < 4 bytes long', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = '0x123456';
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: authorized,
+ }),
+ RevertReason.LengthGreaterThan3Required,
+ );
+ });
+ it('should revert if caller is not authorized', async () => {
+ const inputAmount = new BigNumber(1);
+ const erc20Amount = new BigNumber(10);
+ const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address);
+ const erc721Amount = new BigNumber(1);
+ const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId);
+ const amounts = [erc20Amount, erc721Amount];
+ const nestedAssetData = [erc20AssetData, erc721AssetData];
+ const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData);
+ const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData(
+ assetData,
+ fromAddress,
+ toAddress,
+ inputAmount,
+ );
+ await expectTransactionFailedAsync(
+ web3Wrapper.sendTransactionAsync({
+ to: multiAssetProxy.address,
+ data,
+ from: notAuthorized,
+ }),
+ RevertReason.SenderNotAuthorized,
+ );
+ });
+ });
+ });
+});
+// tslint:enable:no-unnecessary-type-assertion
+// tslint:disable:max-file-line-count
diff --git a/contracts/protocol/test/exchange/core.ts b/contracts/protocol/test/exchange/core.ts
new file mode 100644
index 000000000..9b948f991
--- /dev/null
+++ b/contracts/protocol/test/exchange/core.ts
@@ -0,0 +1,1178 @@
+import { artifacts as interfacesArtifacts, IAssetDataContract } from '@0x/contracts-interfaces';
+import {
+ chaiSetup,
+ constants,
+ ERC20BalancesByOwner,
+ expectTransactionFailedAsync,
+ getLatestBlockTimestampAsync,
+ increaseTimeAndMineBlockAsync,
+ OrderFactory,
+ OrderStatus,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import {
+ artifacts as tokensArtifacts,
+ DummyERC20TokenContract,
+ DummyERC20TokenTransferEventArgs,
+ DummyERC721TokenContract,
+ DummyNoReturnERC20TokenContract,
+ ReentrantERC20TokenContract,
+} from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils, orderHashUtils } from '@0x/order-utils';
+import { RevertReason, SignatureType, SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as chai from 'chai';
+import { LogWithDecodedArgs } from 'ethereum-types';
+import ethUtil = require('ethereumjs-util');
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import { ExchangeCancelEventArgs, ExchangeContract } from '../../generated-wrappers/exchange';
+import { MultiAssetProxyContract } from '../../generated-wrappers/multi_asset_proxy';
+import { TestStaticCallReceiverContract } from '../../generated-wrappers/test_static_call_receiver';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+import { ExchangeWrapper } from '../utils/exchange_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+const assetDataInterface = new IAssetDataContract(
+ interfacesArtifacts.IAssetData.compilerOutput.abi,
+ constants.NULL_ADDRESS,
+ provider,
+);
+// tslint:disable:no-unnecessary-type-assertion
+describe('Exchange core', () => {
+ let makerAddress: string;
+ let owner: string;
+ let takerAddress: string;
+ let feeRecipientAddress: string;
+
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc20TokenB: DummyERC20TokenContract;
+ let zrxToken: DummyERC20TokenContract;
+ let erc721Token: DummyERC721TokenContract;
+ let noReturnErc20Token: DummyNoReturnERC20TokenContract;
+ let reentrantErc20Token: ReentrantERC20TokenContract;
+ let exchange: ExchangeContract;
+ let erc20Proxy: ERC20ProxyContract;
+ let erc721Proxy: ERC721ProxyContract;
+ let multiAssetProxy: MultiAssetProxyContract;
+ let maliciousWallet: TestStaticCallReceiverContract;
+ let maliciousValidator: TestStaticCallReceiverContract;
+
+ let signedOrder: SignedOrder;
+ let erc20Balances: ERC20BalancesByOwner;
+ let exchangeWrapper: ExchangeWrapper;
+ let erc20Wrapper: ERC20Wrapper;
+ let erc721Wrapper: ERC721Wrapper;
+ let orderFactory: OrderFactory;
+
+ let erc721MakerAssetIds: BigNumber[];
+ let erc721TakerAssetIds: BigNumber[];
+
+ let defaultMakerAssetAddress: string;
+ let defaultTakerAssetAddress: string;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = _.slice(accounts, 0, 4));
+
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+ erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+
+ // Deploy AssetProxies, Exchange, tokens, and malicious contracts
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync(
+ artifacts.MultiAssetProxy,
+ provider,
+ txDefaults,
+ );
+ const numDummyErc20ToDeploy = 3;
+ [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
+ exchange = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ );
+ maliciousWallet = maliciousValidator = await TestStaticCallReceiverContract.deployFrom0xArtifactAsync(
+ artifacts.TestStaticCallReceiver,
+ provider,
+ txDefaults,
+ );
+ reentrantErc20Token = await ReentrantERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.ReentrantERC20Token,
+ provider,
+ txDefaults,
+ exchange.address,
+ );
+
+ // Configure ERC20Proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Configure ERC721Proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Configure MultiAssetProxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc721Proxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ // Configure Exchange
+ exchangeWrapper = new ExchangeWrapper(exchange, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
+ await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner);
+ await exchangeWrapper.registerAssetProxyAsync(multiAssetProxy.address, owner);
+
+ // Configure ERC20 tokens
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ // Configure ERC721 tokens
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address];
+ erc721TakerAssetIds = erc721Balances[takerAddress][erc721Token.address];
+
+ // Configure order defaults
+ defaultMakerAssetAddress = erc20TokenA.address;
+ defaultTakerAssetAddress = erc20TokenB.address;
+ const defaultOrderParams = {
+ ...constants.STATIC_ORDER_PARAMS,
+ exchangeAddress: exchange.address,
+ makerAddress,
+ feeRecipientAddress,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress),
+ };
+ const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
+ orderFactory = new OrderFactory(privateKey, defaultOrderParams);
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('fillOrder', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ });
+
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow fillOrder to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('fillOrder reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should throw if signature is invalid', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+
+ const v = ethUtil.toBuffer(signedOrder.signature.slice(0, 4));
+ const invalidR = ethUtil.sha3('invalidR');
+ const invalidS = ethUtil.sha3('invalidS');
+ const signatureType = ethUtil.toBuffer(`0x${signedOrder.signature.slice(-2)}`);
+ const invalidSigBuff = Buffer.concat([v, invalidR, invalidS, signatureType]);
+ const invalidSigHex = `0x${invalidSigBuff.toString('hex')}`;
+ signedOrder.signature = invalidSigHex;
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
+ RevertReason.InvalidOrderSignature,
+ );
+ });
+
+ it('should throw if no value is filled', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should revert if `isValidSignature` tries to update state when SignatureType=Wallet', async () => {
+ const maliciousMakerAddress = maliciousWallet.address;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenA.setBalance.sendTransactionAsync(
+ maliciousMakerAddress,
+ constants.INITIAL_ERC20_BALANCE,
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await maliciousWallet.approveERC20.sendTransactionAsync(
+ erc20TokenA.address,
+ erc20Proxy.address,
+ constants.INITIAL_ERC20_ALLOWANCE,
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAddress: maliciousMakerAddress,
+ makerFee: constants.ZERO_AMOUNT,
+ });
+ signedOrder.signature = `0x0${SignatureType.Wallet}`;
+ await expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
+ RevertReason.WalletError,
+ );
+ });
+
+ it('should revert if `isValidSignature` tries to update state when SignatureType=Validator', async () => {
+ const isApproved = true;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await exchange.setSignatureValidatorApproval.sendTransactionAsync(
+ maliciousValidator.address,
+ isApproved,
+ { from: makerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ signedOrder.signature = `${maliciousValidator.address}0${SignatureType.Validator}`;
+ await expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress),
+ RevertReason.ValidatorError,
+ );
+ });
+
+ it('should not emit transfer events for transfers where from == to', async () => {
+ const txReceipt = await exchangeWrapper.fillOrderAsync(signedOrder, makerAddress);
+ const logs = txReceipt.logs;
+ const transferLogs = _.filter(
+ logs,
+ log => (log as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).event === 'Transfer',
+ );
+ expect(transferLogs.length).to.be.equal(2);
+ expect((transferLogs[0] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).address).to.be.equal(
+ zrxToken.address,
+ );
+ expect((transferLogs[0] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._from).to.be.equal(
+ makerAddress,
+ );
+ expect((transferLogs[0] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._to).to.be.equal(
+ feeRecipientAddress,
+ );
+ expect(
+ (transferLogs[0] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._value,
+ ).to.be.bignumber.equal(signedOrder.makerFee);
+ expect((transferLogs[1] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).address).to.be.equal(
+ zrxToken.address,
+ );
+ expect((transferLogs[1] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._from).to.be.equal(
+ makerAddress,
+ );
+ expect((transferLogs[1] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._to).to.be.equal(
+ feeRecipientAddress,
+ );
+ expect(
+ (transferLogs[1] as LogWithDecodedArgs<DummyERC20TokenTransferEventArgs>).args._value,
+ ).to.be.bignumber.equal(signedOrder.takerFee);
+ });
+ });
+
+ describe('Testing exchange of ERC20 tokens with no return values', () => {
+ before(async () => {
+ noReturnErc20Token = await DummyNoReturnERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyNoReturnERC20Token,
+ provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ constants.DUMMY_TOKEN_DECIMALS,
+ constants.DUMMY_TOKEN_TOTAL_SUPPLY,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await noReturnErc20Token.setBalance.sendTransactionAsync(makerAddress, constants.INITIAL_ERC20_BALANCE),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await noReturnErc20Token.approve.sendTransactionAsync(
+ erc20Proxy.address,
+ constants.INITIAL_ERC20_ALLOWANCE,
+ { from: makerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ });
+ it('should transfer the correct amounts when makerAssetAmount === takerAssetAmount', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+
+ const initialMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const initialFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+
+ const finalMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const finalFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(initialMakerBalanceA.minus(signedOrder.makerAssetAmount));
+ expect(finalMakerBalanceB).to.be.bignumber.equal(initialMakerBalanceB.plus(signedOrder.takerAssetAmount));
+ expect(finalTakerBalanceA).to.be.bignumber.equal(initialTakerBalanceA.plus(signedOrder.makerAssetAmount));
+ expect(finalTakerBalanceB).to.be.bignumber.equal(initialTakerBalanceB.minus(signedOrder.takerAssetAmount));
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.minus(signedOrder.makerFee));
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(signedOrder.takerFee));
+ expect(finalFeeRecipientZrxBalance).to.be.bignumber.equal(
+ initialFeeRecipientZrxBalance.plus(signedOrder.makerFee.plus(signedOrder.takerFee)),
+ );
+ });
+ it('should transfer the correct amounts when makerAssetAmount > takerAssetAmount', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+
+ const initialMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const initialFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+
+ const finalMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const finalFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(initialMakerBalanceA.minus(signedOrder.makerAssetAmount));
+ expect(finalMakerBalanceB).to.be.bignumber.equal(initialMakerBalanceB.plus(signedOrder.takerAssetAmount));
+ expect(finalTakerBalanceA).to.be.bignumber.equal(initialTakerBalanceA.plus(signedOrder.makerAssetAmount));
+ expect(finalTakerBalanceB).to.be.bignumber.equal(initialTakerBalanceB.minus(signedOrder.takerAssetAmount));
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.minus(signedOrder.makerFee));
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(signedOrder.takerFee));
+ expect(finalFeeRecipientZrxBalance).to.be.bignumber.equal(
+ initialFeeRecipientZrxBalance.plus(signedOrder.makerFee.plus(signedOrder.takerFee)),
+ );
+ });
+ it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
+ });
+
+ const initialMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const initialFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+
+ const finalMakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await noReturnErc20Token.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const finalFeeRecipientZrxBalance = await zrxToken.balanceOf.callAsync(feeRecipientAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(initialMakerBalanceA.minus(signedOrder.makerAssetAmount));
+ expect(finalMakerBalanceB).to.be.bignumber.equal(initialMakerBalanceB.plus(signedOrder.takerAssetAmount));
+ expect(finalTakerBalanceA).to.be.bignumber.equal(initialTakerBalanceA.plus(signedOrder.makerAssetAmount));
+ expect(finalTakerBalanceB).to.be.bignumber.equal(initialTakerBalanceB.minus(signedOrder.takerAssetAmount));
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.minus(signedOrder.makerFee));
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(signedOrder.takerFee));
+ expect(finalFeeRecipientZrxBalance).to.be.bignumber.equal(
+ initialFeeRecipientZrxBalance.plus(signedOrder.makerFee.plus(signedOrder.takerFee)),
+ );
+ });
+ });
+
+ describe('cancelOrder', () => {
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ });
+
+ it('should throw if not sent by maker', async () => {
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrderAsync(signedOrder, takerAddress),
+ RevertReason.InvalidMaker,
+ );
+ });
+
+ it('should throw if makerAssetAmount is 0', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(0),
+ });
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should throw if takerAssetAmount is 0', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ takerAssetAmount: new BigNumber(0),
+ });
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should be able to cancel a full order', async () => {
+ await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount: signedOrder.takerAssetAmount.div(2),
+ }),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should log 1 event with correct arguments', async () => {
+ const res = await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress);
+ expect(res.logs).to.have.length(1);
+
+ const log = res.logs[0] as LogWithDecodedArgs<ExchangeCancelEventArgs>;
+ const logArgs = log.args;
+
+ expect(signedOrder.makerAddress).to.be.equal(logArgs.makerAddress);
+ expect(signedOrder.makerAddress).to.be.equal(logArgs.senderAddress);
+ expect(signedOrder.feeRecipientAddress).to.be.equal(logArgs.feeRecipientAddress);
+ expect(signedOrder.makerAssetData).to.be.equal(logArgs.makerAssetData);
+ expect(signedOrder.takerAssetData).to.be.equal(logArgs.takerAssetData);
+ expect(orderHashUtils.getOrderHashHex(signedOrder)).to.be.equal(logArgs.orderHash);
+ });
+
+ it('should throw if already cancelled', async () => {
+ await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should throw if order is expired', async () => {
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ expirationTimeSeconds: new BigNumber(currentTimestamp).sub(10),
+ });
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should throw if rounding error is greater than 0.1%', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1001),
+ takerAssetAmount: new BigNumber(3),
+ });
+
+ const fillTakerAssetAmount1 = new BigNumber(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount: fillTakerAssetAmount1,
+ });
+
+ const fillTakerAssetAmount2 = new BigNumber(1);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount: fillTakerAssetAmount2,
+ }),
+ RevertReason.RoundingError,
+ );
+ });
+ });
+
+ describe('cancelOrdersUpTo', () => {
+ it('should fail to set orderEpoch less than current orderEpoch', async () => {
+ const orderEpoch = new BigNumber(1);
+ await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
+ const lesserOrderEpoch = new BigNumber(0);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrdersUpToAsync(lesserOrderEpoch, makerAddress),
+ RevertReason.InvalidNewOrderEpoch,
+ );
+ });
+
+ it('should fail to set orderEpoch equal to existing orderEpoch', async () => {
+ const orderEpoch = new BigNumber(1);
+ await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress),
+ RevertReason.InvalidNewOrderEpoch,
+ );
+ });
+
+ it('should cancel only orders with a orderEpoch less than existing orderEpoch', async () => {
+ // Cancel all transactions with a orderEpoch less than 1
+ const orderEpoch = new BigNumber(1);
+ await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
+
+ // Create 3 orders with orderEpoch values: 0,1,2,3
+ // Since we cancelled with orderEpoch=1, orders with orderEpoch<=1 will not be processed
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const signedOrders = [
+ await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(9), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(9), 18),
+ salt: new BigNumber(0),
+ }),
+ await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(79), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(79), 18),
+ salt: new BigNumber(1),
+ }),
+ await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(979), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(979), 18),
+ salt: new BigNumber(2),
+ }),
+ await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(7979), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(7979), 18),
+ salt: new BigNumber(3),
+ }),
+ ];
+ await exchangeWrapper.batchFillOrdersNoThrowAsync(signedOrders, takerAddress, {
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const fillMakerAssetAmount = signedOrders[2].makerAssetAmount.add(signedOrders[3].makerAssetAmount);
+ const fillTakerAssetAmount = signedOrders[2].takerAssetAmount.add(signedOrders[3].takerAssetAmount);
+ const makerFee = signedOrders[2].makerFee.add(signedOrders[3].makerFee);
+ const takerFee = signedOrders[2].takerFee.add(signedOrders[3].takerFee);
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(fillMakerAssetAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(fillTakerAssetAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(fillTakerAssetAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(fillMakerAssetAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+ });
+
+ describe('Testing Exchange of ERC721 Tokens', () => {
+ it('should throw when maker does not own the token with id makerAssetId', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721TakerAssetIds[0];
+ const takerAssetId = erc721TakerAssetIds[1];
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ });
+ // Verify pre-conditions
+ const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(initialOwnerMakerAsset).to.be.bignumber.not.equal(makerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }),
+ RevertReason.TransferFailed,
+ );
+ });
+
+ it('should throw when taker does not own the token with id takerAssetId', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721MakerAssetIds[0];
+ const takerAssetId = erc721MakerAssetIds[1];
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ });
+ // Verify pre-conditions
+ const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.not.equal(takerAddress);
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }),
+ RevertReason.TransferFailed,
+ );
+ });
+
+ it('should throw when makerAssetAmount is greater than 1', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721MakerAssetIds[0];
+ const takerAssetId = erc721TakerAssetIds[0];
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(2),
+ takerAssetAmount: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ });
+ // Verify pre-conditions
+ const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }),
+ RevertReason.InvalidAmount,
+ );
+ });
+
+ it('should throw when takerAssetAmount is greater than 1', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721MakerAssetIds[0];
+ const takerAssetId = erc721TakerAssetIds[0];
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: new BigNumber(500),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ });
+ // Verify pre-conditions
+ const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }),
+ RevertReason.InvalidAmount,
+ );
+ });
+
+ it('should throw on partial fill', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721MakerAssetIds[0];
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress),
+ });
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }),
+ RevertReason.RoundingError,
+ );
+ });
+ });
+
+ describe('Testing exchange of multiple assets', () => {
+ it('should allow multiple assets to be exchanged for a single asset', async () => {
+ const makerAmounts = [new BigNumber(10), new BigNumber(20)];
+ const makerNestedAssetData = [
+ assetDataUtils.encodeERC20AssetData(erc20TokenA.address),
+ assetDataUtils.encodeERC20AssetData(erc20TokenB.address),
+ ];
+ const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ makerAmounts,
+ makerNestedAssetData,
+ );
+ const makerAssetAmount = new BigNumber(1);
+ const takerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+ const takerAssetAmount = new BigNumber(10);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData,
+ takerAssetData,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee: constants.ZERO_AMOUNT,
+ takerFee: constants.ZERO_AMOUNT,
+ });
+
+ const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+
+ const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(
+ initialMakerBalanceA.minus(makerAmounts[0].times(makerAssetAmount)),
+ );
+ expect(finalMakerBalanceB).to.be.bignumber.equal(
+ initialMakerBalanceB.minus(makerAmounts[1].times(makerAssetAmount)),
+ );
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.plus(takerAssetAmount));
+ expect(finalTakerBalanceA).to.be.bignumber.equal(
+ initialTakerBalanceA.plus(makerAmounts[0].times(makerAssetAmount)),
+ );
+ expect(finalTakerBalanceB).to.be.bignumber.equal(
+ initialTakerBalanceB.plus(makerAmounts[1].times(makerAssetAmount)),
+ );
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(takerAssetAmount));
+ });
+ it('should allow multiple assets to be exchanged for multiple assets', async () => {
+ const makerAmounts = [new BigNumber(10), new BigNumber(20)];
+ const makerNestedAssetData = [
+ assetDataUtils.encodeERC20AssetData(erc20TokenA.address),
+ assetDataUtils.encodeERC20AssetData(erc20TokenB.address),
+ ];
+ const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ makerAmounts,
+ makerNestedAssetData,
+ );
+ const makerAssetAmount = new BigNumber(1);
+ const takerAmounts = [new BigNumber(10), new BigNumber(1)];
+ const takerAssetId = erc721TakerAssetIds[0];
+ const takerNestedAssetData = [
+ assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ ];
+ const takerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ takerAmounts,
+ takerNestedAssetData,
+ );
+ const takerAssetAmount = new BigNumber(1);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData,
+ takerAssetData,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee: constants.ZERO_AMOUNT,
+ takerFee: constants.ZERO_AMOUNT,
+ });
+
+ const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+
+ const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+ const finalOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(
+ initialMakerBalanceA.minus(makerAmounts[0].times(makerAssetAmount)),
+ );
+ expect(finalMakerBalanceB).to.be.bignumber.equal(
+ initialMakerBalanceB.minus(makerAmounts[1].times(makerAssetAmount)),
+ );
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(
+ initialMakerZrxBalance.plus(takerAmounts[0].times(takerAssetAmount)),
+ );
+ expect(finalTakerBalanceA).to.be.bignumber.equal(
+ initialTakerBalanceA.plus(makerAmounts[0].times(makerAssetAmount)),
+ );
+ expect(finalTakerBalanceB).to.be.bignumber.equal(
+ initialTakerBalanceB.plus(makerAmounts[1].times(makerAssetAmount)),
+ );
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(
+ initialTakerZrxBalance.minus(takerAmounts[0].times(takerAssetAmount)),
+ );
+ expect(finalOwnerTakerAsset).to.be.equal(makerAddress);
+ });
+ it('should allow an order selling multiple assets to be partially filled', async () => {
+ const makerAmounts = [new BigNumber(10), new BigNumber(20)];
+ const makerNestedAssetData = [
+ assetDataUtils.encodeERC20AssetData(erc20TokenA.address),
+ assetDataUtils.encodeERC20AssetData(erc20TokenB.address),
+ ];
+ const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ makerAmounts,
+ makerNestedAssetData,
+ );
+ const makerAssetAmount = new BigNumber(30);
+ const takerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+ const takerAssetAmount = new BigNumber(10);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData,
+ takerAssetData,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee: constants.ZERO_AMOUNT,
+ takerFee: constants.ZERO_AMOUNT,
+ });
+
+ const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ const takerAssetFillAmount = takerAssetAmount.dividedToIntegerBy(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(
+ initialMakerBalanceA.minus(
+ makerAmounts[0].times(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalMakerBalanceB).to.be.bignumber.equal(
+ initialMakerBalanceB.minus(
+ makerAmounts[1].times(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(
+ initialMakerZrxBalance.plus(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ );
+ expect(finalTakerBalanceA).to.be.bignumber.equal(
+ initialTakerBalanceA.plus(
+ makerAmounts[0].times(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalTakerBalanceB).to.be.bignumber.equal(
+ initialTakerBalanceB.plus(
+ makerAmounts[1].times(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(
+ initialTakerZrxBalance.minus(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ );
+ });
+ it('should allow an order buying multiple assets to be partially filled', async () => {
+ const takerAmounts = [new BigNumber(10), new BigNumber(20)];
+ const takerNestedAssetData = [
+ assetDataUtils.encodeERC20AssetData(erc20TokenA.address),
+ assetDataUtils.encodeERC20AssetData(erc20TokenB.address),
+ ];
+ const takerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(
+ takerAmounts,
+ takerNestedAssetData,
+ );
+ const takerAssetAmount = new BigNumber(30);
+ const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+ const makerAssetAmount = new BigNumber(10);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData,
+ takerAssetData,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee: constants.ZERO_AMOUNT,
+ takerFee: constants.ZERO_AMOUNT,
+ });
+
+ const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ const takerAssetFillAmount = takerAssetAmount.dividedToIntegerBy(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress);
+ const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress);
+ const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress);
+ const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress);
+ const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress);
+ const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress);
+
+ expect(finalMakerBalanceA).to.be.bignumber.equal(
+ initialMakerBalanceA.plus(
+ takerAmounts[0].times(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalMakerBalanceB).to.be.bignumber.equal(
+ initialMakerBalanceB.plus(
+ takerAmounts[1].times(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalMakerZrxBalance).to.be.bignumber.equal(
+ initialMakerZrxBalance.minus(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ );
+ expect(finalTakerBalanceA).to.be.bignumber.equal(
+ initialTakerBalanceA.minus(
+ takerAmounts[0].times(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalTakerBalanceB).to.be.bignumber.equal(
+ initialTakerBalanceB.minus(
+ takerAmounts[1].times(
+ takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ ),
+ );
+ expect(finalTakerZrxBalance).to.be.bignumber.equal(
+ initialTakerZrxBalance.plus(
+ makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount),
+ ),
+ );
+ });
+ });
+
+ describe('getOrderInfo', () => {
+ beforeEach(async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ });
+ it('should return the correct orderInfo for an unfilled valid order', async () => {
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.FILLABLE;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for a fully filled order', async () => {
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount;
+ const expectedOrderStatus = OrderStatus.FULLY_FILLED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for a partially filled order', async () => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = takerAssetFillAmount;
+ const expectedOrderStatus = OrderStatus.FILLABLE;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for a cancelled and unfilled order', async () => {
+ await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.CANCELLED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for a cancelled and partially filled order', async () => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
+ await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = takerAssetFillAmount;
+ const expectedOrderStatus = OrderStatus.CANCELLED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for an expired and unfilled order', async () => {
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const timeUntilExpiration = signedOrder.expirationTimeSeconds.minus(currentTimestamp).toNumber();
+ await increaseTimeAndMineBlockAsync(timeUntilExpiration);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.EXPIRED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for an expired and partially filled order', async () => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount });
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const timeUntilExpiration = signedOrder.expirationTimeSeconds.minus(currentTimestamp).toNumber();
+ await increaseTimeAndMineBlockAsync(timeUntilExpiration);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = takerAssetFillAmount;
+ const expectedOrderStatus = OrderStatus.EXPIRED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for an expired and fully filled order', async () => {
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress);
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const timeUntilExpiration = signedOrder.expirationTimeSeconds.minus(currentTimestamp).toNumber();
+ await increaseTimeAndMineBlockAsync(timeUntilExpiration);
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount;
+ // FULLY_FILLED takes precedence over EXPIRED
+ const expectedOrderStatus = OrderStatus.FULLY_FILLED;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for an order with a makerAssetAmount of 0', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({ makerAssetAmount: new BigNumber(0) });
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.INVALID_MAKER_ASSET_AMOUNT;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ it('should return the correct orderInfo for an order with a takerAssetAmount of 0', async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync({ takerAssetAmount: new BigNumber(0) });
+ const orderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrder);
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.INVALID_TAKER_ASSET_AMOUNT;
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+});
+// tslint:disable:max-file-line-count
+// tslint:enable:no-unnecessary-type-assertion
diff --git a/contracts/protocol/test/exchange/dispatcher.ts b/contracts/protocol/test/exchange/dispatcher.ts
new file mode 100644
index 000000000..5b1ac1e20
--- /dev/null
+++ b/contracts/protocol/test/exchange/dispatcher.ts
@@ -0,0 +1,284 @@
+import {
+ chaiSetup,
+ constants,
+ expectTransactionFailedAsync,
+ LogDecoder,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { DummyERC20TokenContract } from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils } from '@0x/order-utils';
+import { AssetProxyId, RevertReason } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import { LogWithDecodedArgs } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import {
+ TestAssetProxyDispatcherAssetProxyRegisteredEventArgs,
+ TestAssetProxyDispatcherContract,
+} from '../../generated-wrappers/test_asset_proxy_dispatcher';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+// tslint:disable:no-unnecessary-type-assertion
+describe('AssetProxyDispatcher', () => {
+ let owner: string;
+ let notOwner: string;
+ let makerAddress: string;
+ let takerAddress: string;
+
+ let zrxToken: DummyERC20TokenContract;
+ let erc20Proxy: ERC20ProxyContract;
+ let erc721Proxy: ERC721ProxyContract;
+ let assetProxyDispatcher: TestAssetProxyDispatcherContract;
+
+ let erc20Wrapper: ERC20Wrapper;
+ let erc721Wrapper: ERC721Wrapper;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ // Setup accounts & addresses
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, notOwner, makerAddress, takerAddress] = _.slice(accounts, 0, 4));
+
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+ erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+
+ const numDummyErc20ToDeploy = 1;
+ [zrxToken] = await erc20Wrapper.deployDummyTokensAsync(numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS);
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ erc721Proxy = await erc721Wrapper.deployProxyAsync();
+
+ assetProxyDispatcher = await TestAssetProxyDispatcherContract.deployFrom0xArtifactAsync(
+ artifacts.TestAssetProxyDispatcher,
+ provider,
+ txDefaults,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('registerAssetProxy', () => {
+ it('should record proxy upon registration', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20);
+ expect(proxyAddress).to.be.equal(erc20Proxy.address);
+ });
+
+ it('should be able to record multiple proxies', async () => {
+ // Record first proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ let proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20);
+ expect(proxyAddress).to.be.equal(erc20Proxy.address);
+ // Record another proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc721Proxy.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC721);
+ expect(proxyAddress).to.be.equal(erc721Proxy.address);
+ });
+
+ it('should throw if a proxy with the same id is already registered', async () => {
+ // Initial registration
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20);
+ expect(proxyAddress).to.be.equal(erc20Proxy.address);
+ // Deploy a new version of the ERC20 Transfer Proxy contract
+ const newErc20TransferProxy = await ERC20ProxyContract.deployFrom0xArtifactAsync(
+ artifacts.ERC20Proxy,
+ provider,
+ txDefaults,
+ );
+ // Register new ERC20 Transfer Proxy contract
+ return expectTransactionFailedAsync(
+ assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(newErc20TransferProxy.address, {
+ from: owner,
+ }),
+ RevertReason.AssetProxyAlreadyExists,
+ );
+ });
+
+ it('should throw if requesting address is not owner', async () => {
+ return expectTransactionFailedAsync(
+ assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: notOwner }),
+ RevertReason.OnlyContractOwner,
+ );
+ });
+
+ it('should log an event with correct arguments when an asset proxy is registered', async () => {
+ const logDecoder = new LogDecoder(web3Wrapper, artifacts);
+ const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ );
+ const logs = txReceipt.logs;
+ const log = logs[0] as LogWithDecodedArgs<TestAssetProxyDispatcherAssetProxyRegisteredEventArgs>;
+ expect(log.args.id).to.equal(AssetProxyId.ERC20);
+ expect(log.args.assetProxy).to.equal(erc20Proxy.address);
+ });
+ });
+
+ describe('getAssetProxy', () => {
+ it('should return correct address of registered proxy', async () => {
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20);
+ expect(proxyAddress).to.be.equal(erc20Proxy.address);
+ });
+
+ it('should return NULL address if requesting non-existent proxy', async () => {
+ const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20);
+ expect(proxyAddress).to.be.equal(constants.NULL_ADDRESS);
+ });
+ });
+
+ describe('dispatchTransferFrom', () => {
+ it('should dispatch transfer to registered proxy', async () => {
+ // Register ERC20 proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Construct metadata for ERC20 proxy
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+
+ // Perform a transfer from makerAddress to takerAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = new BigNumber(10);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync(
+ encodedAssetData,
+ makerAddress,
+ takerAddress,
+ amount,
+ { from: owner },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Verify transfer was successful
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(amount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].add(amount),
+ );
+ });
+
+ it('should not dispatch a transfer if amount == 0', async () => {
+ // Register ERC20 proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Construct metadata for ERC20 proxy
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+
+ // Perform a transfer from makerAddress to takerAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = constants.ZERO_AMOUNT;
+ const txReceipt = await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync(
+ encodedAssetData,
+ makerAddress,
+ takerAddress,
+ amount,
+ { from: owner },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ expect(txReceipt.logs.length).to.be.equal(0);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.deep.equal(erc20Balances);
+ });
+
+ it('should not dispatch a transfer if from == to', async () => {
+ // Register ERC20 proxy
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Construct metadata for ERC20 proxy
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+
+ // Perform a transfer from makerAddress to takerAddress
+ const erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const amount = new BigNumber(10);
+ const txReceipt = await web3Wrapper.awaitTransactionSuccessAsync(
+ await assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync(
+ encodedAssetData,
+ makerAddress,
+ makerAddress,
+ amount,
+ { from: owner },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ expect(txReceipt.logs.length).to.be.equal(0);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.deep.equal(erc20Balances);
+ });
+
+ it('should throw if dispatching to unregistered proxy', async () => {
+ // Construct metadata for ERC20 proxy
+ const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+ // Perform a transfer from makerAddress to takerAddress
+ const amount = new BigNumber(10);
+ return expectTransactionFailedAsync(
+ assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync(
+ encodedAssetData,
+ makerAddress,
+ takerAddress,
+ amount,
+ { from: owner },
+ ),
+ RevertReason.AssetProxyDoesNotExist,
+ );
+ });
+ });
+});
+// tslint:enable:no-unnecessary-type-assertion
diff --git a/contracts/protocol/test/exchange/fill_order.ts b/contracts/protocol/test/exchange/fill_order.ts
new file mode 100644
index 000000000..2bdbe4855
--- /dev/null
+++ b/contracts/protocol/test/exchange/fill_order.ts
@@ -0,0 +1,314 @@
+import {
+ AllowanceAmountScenario,
+ AssetDataScenario,
+ BalanceAmountScenario,
+ chaiSetup,
+ ExpirationTimeSecondsScenario,
+ FeeRecipientAddressScenario,
+ FillScenario,
+ OrderAssetAmountScenario,
+ provider,
+ TakerAssetFillAmountScenario,
+ TakerScenario,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import * as _ from 'lodash';
+
+import {
+ FillOrderCombinatorialUtils,
+ fillOrderCombinatorialUtilsFactoryAsync,
+} from '../utils/fill_order_combinatorial_utils';
+
+chaiSetup.configure();
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+const defaultFillScenario = {
+ orderScenario: {
+ takerScenario: TakerScenario.Unspecified,
+ feeRecipientScenario: FeeRecipientAddressScenario.EthUserAddress,
+ makerAssetAmountScenario: OrderAssetAmountScenario.Large,
+ takerAssetAmountScenario: OrderAssetAmountScenario.Large,
+ makerFeeScenario: OrderAssetAmountScenario.Large,
+ takerFeeScenario: OrderAssetAmountScenario.Large,
+ expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InFuture,
+ makerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount,
+ makerStateScenario: {
+ traderAssetBalance: BalanceAmountScenario.Higher,
+ traderAssetAllowance: AllowanceAmountScenario.Higher,
+ zrxFeeBalance: BalanceAmountScenario.Higher,
+ zrxFeeAllowance: AllowanceAmountScenario.Higher,
+ },
+ takerStateScenario: {
+ traderAssetBalance: BalanceAmountScenario.Higher,
+ traderAssetAllowance: AllowanceAmountScenario.Higher,
+ zrxFeeBalance: BalanceAmountScenario.Higher,
+ zrxFeeAllowance: AllowanceAmountScenario.Higher,
+ },
+};
+
+describe('FillOrder Tests', () => {
+ let fillOrderCombinatorialUtils: FillOrderCombinatorialUtils;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ fillOrderCombinatorialUtils = await fillOrderCombinatorialUtilsFactoryAsync(web3Wrapper, txDefaults);
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('fillOrder', () => {
+ const test = (fillScenarios: FillScenario[]) => {
+ _.forEach(fillScenarios, fillScenario => {
+ const description = `Combinatorial OrderFill: ${JSON.stringify(fillScenario)}`;
+ it(description, async () => {
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ });
+ };
+
+ const allFillScenarios = FillOrderCombinatorialUtils.generateFillOrderCombinations();
+ describe('Combinatorially generated fills orders', () => test(allFillScenarios));
+
+ it('should transfer the correct amounts when makerAssetAmount === takerAssetAmount', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should transfer the correct amounts when makerAssetAmount > takerAssetAmount', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ takerAssetAmountScenario: OrderAssetAmountScenario.Small,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetAmountScenario: OrderAssetAmountScenario.Small,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount with zero decimals', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetAmountScenario: OrderAssetAmountScenario.Small,
+ makerAssetDataScenario: AssetDataScenario.ERC20ZeroDecimals,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ takerScenario: TakerScenario.CorrectlySpecified,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should fill remaining value if takerAssetFillAmount > remaining takerAssetAmount', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ it('should throw when taker is specified and order is claimed by other', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ takerScenario: TakerScenario.IncorrectlySpecified,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if makerAssetAmount is 0', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetAmountScenario: OrderAssetAmountScenario.Zero,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if takerAssetAmount is 0', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ takerAssetAmountScenario: OrderAssetAmountScenario.Zero,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if takerAssetFillAmount is 0', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.Zero,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if an order is expired', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InPast,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if maker erc20Balances are too low to fill order', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ makerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetBalance: BalanceAmountScenario.TooLow,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if taker erc20Balances are too low to fill order', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ takerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetBalance: BalanceAmountScenario.TooLow,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if maker allowances are too low to fill order', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ makerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetAllowance: AllowanceAmountScenario.TooLow,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should throw if taker allowances are too low to fill order', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ takerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetAllowance: AllowanceAmountScenario.TooLow,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ });
+
+ describe('Testing exchange of ERC721 Tokens', () => {
+ it('should successfully exchange a single token between the maker and taker (via fillOrder)', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetDataScenario: AssetDataScenario.ERC721,
+ takerAssetDataScenario: AssetDataScenario.ERC721,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should successfully fill order when makerAsset is ERC721 and takerAsset is ERC20', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetDataScenario: AssetDataScenario.ERC721,
+ takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario, true);
+ });
+
+ it('should successfully fill order when makerAsset is ERC20 and takerAsset is ERC721', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ takerAssetDataScenario: AssetDataScenario.ERC721,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should successfully fill order when makerAsset is ERC721 and approveAll is set for it', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetDataScenario: AssetDataScenario.ERC721,
+ takerAssetDataScenario: AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ makerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetAllowance: AllowanceAmountScenario.Unlimited,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+
+ it('should successfully fill order when makerAsset and takerAsset are ERC721 and approveAll is set for them', async () => {
+ const fillScenario = {
+ ...defaultFillScenario,
+ orderScenario: {
+ ...defaultFillScenario.orderScenario,
+ makerAssetDataScenario: AssetDataScenario.ERC721,
+ takerAssetDataScenario: AssetDataScenario.ERC721,
+ },
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ makerStateScenario: {
+ ...defaultFillScenario.makerStateScenario,
+ traderAssetAllowance: AllowanceAmountScenario.Unlimited,
+ },
+ takerStateScenario: {
+ ...defaultFillScenario.takerStateScenario,
+ traderAssetAllowance: AllowanceAmountScenario.Unlimited,
+ },
+ };
+ await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
+ });
+ });
+});
diff --git a/contracts/protocol/test/exchange/internal.ts b/contracts/protocol/test/exchange/internal.ts
new file mode 100644
index 000000000..972f5efb6
--- /dev/null
+++ b/contracts/protocol/test/exchange/internal.ts
@@ -0,0 +1,472 @@
+import {
+ bytes32Values,
+ chaiSetup,
+ constants,
+ FillResults,
+ getRevertReasonOrErrorMessageForSendTransactionAsync,
+ provider,
+ testCombinatoriallyWithReferenceFuncAsync,
+ txDefaults,
+ uint256Values,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { Order, RevertReason, SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { TestExchangeInternalsContract } from '../../generated-wrappers/test_exchange_internals';
+import { artifacts } from '../../src/artifacts';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
+
+const emptyOrder: Order = {
+ senderAddress: constants.NULL_ADDRESS,
+ makerAddress: constants.NULL_ADDRESS,
+ takerAddress: constants.NULL_ADDRESS,
+ makerFee: new BigNumber(0),
+ takerFee: new BigNumber(0),
+ makerAssetAmount: new BigNumber(0),
+ takerAssetAmount: new BigNumber(0),
+ makerAssetData: '0x',
+ takerAssetData: '0x',
+ salt: new BigNumber(0),
+ exchangeAddress: constants.NULL_ADDRESS,
+ feeRecipientAddress: constants.NULL_ADDRESS,
+ expirationTimeSeconds: new BigNumber(0),
+};
+
+const emptySignedOrder: SignedOrder = {
+ ...emptyOrder,
+ signature: '',
+};
+
+const overflowErrorForCall = new Error(RevertReason.Uint256Overflow);
+
+describe('Exchange core internal functions', () => {
+ let testExchange: TestExchangeInternalsContract;
+ let overflowErrorForSendTransaction: Error | undefined;
+ let divisionByZeroErrorForCall: Error | undefined;
+ let roundingErrorForCall: Error | undefined;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync(
+ artifacts.TestExchangeInternals,
+ provider,
+ txDefaults,
+ );
+ overflowErrorForSendTransaction = new Error(
+ await getRevertReasonOrErrorMessageForSendTransactionAsync(RevertReason.Uint256Overflow),
+ );
+ divisionByZeroErrorForCall = new Error(RevertReason.DivisionByZero);
+ roundingErrorForCall = new Error(RevertReason.RoundingError);
+ });
+ // Note(albrow): Don't forget to add beforeEach and afterEach calls to reset
+ // the blockchain state for any tests which modify it!
+
+ async function referenceIsRoundingErrorFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<boolean> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ if (numerator.eq(0)) {
+ return false;
+ }
+ if (target.eq(0)) {
+ return false;
+ }
+ const product = numerator.mul(target);
+ const remainder = product.mod(denominator);
+ const remainderTimes1000 = remainder.mul('1000');
+ const isError = remainderTimes1000.gte(product);
+ if (product.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ if (remainderTimes1000.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ return isError;
+ }
+
+ async function referenceIsRoundingErrorCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<boolean> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ if (numerator.eq(0)) {
+ return false;
+ }
+ if (target.eq(0)) {
+ return false;
+ }
+ const product = numerator.mul(target);
+ const remainder = product.mod(denominator);
+ const error = denominator.sub(remainder).mod(denominator);
+ const errorTimes1000 = error.mul('1000');
+ const isError = errorTimes1000.gte(product);
+ if (product.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ if (errorTimes1000.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ return isError;
+ }
+
+ async function referenceSafeGetPartialAmountFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ const isRoundingError = await referenceIsRoundingErrorFloorAsync(numerator, denominator, target);
+ if (isRoundingError) {
+ throw roundingErrorForCall;
+ }
+ const product = numerator.mul(target);
+ if (product.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ return product.dividedToIntegerBy(denominator);
+ }
+
+ describe('addFillResults', async () => {
+ function makeFillResults(value: BigNumber): FillResults {
+ return {
+ makerAssetFilledAmount: value,
+ takerAssetFilledAmount: value,
+ makerFeePaid: value,
+ takerFeePaid: value,
+ };
+ }
+ async function referenceAddFillResultsAsync(
+ totalValue: BigNumber,
+ singleValue: BigNumber,
+ ): Promise<FillResults> {
+ // Note(albrow): Here, each of totalFillResults and
+ // singleFillResults will consist of fields with the same values.
+ // This should be safe because none of the fields in a given
+ // FillResults are ever used together in a mathemetical operation.
+ // They are only used with the corresponding field from *the other*
+ // FillResults, which are different.
+ const totalFillResults = makeFillResults(totalValue);
+ const singleFillResults = makeFillResults(singleValue);
+ // HACK(albrow): _.mergeWith mutates the first argument! To
+ // workaround this we use _.cloneDeep.
+ return _.mergeWith(
+ _.cloneDeep(totalFillResults),
+ singleFillResults,
+ (totalVal: BigNumber, singleVal: BigNumber) => {
+ const newTotal = totalVal.add(singleVal);
+ if (newTotal.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ return newTotal;
+ },
+ );
+ }
+ async function testAddFillResultsAsync(totalValue: BigNumber, singleValue: BigNumber): Promise<FillResults> {
+ const totalFillResults = makeFillResults(totalValue);
+ const singleFillResults = makeFillResults(singleValue);
+ return testExchange.publicAddFillResults.callAsync(totalFillResults, singleFillResults);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'addFillResults',
+ referenceAddFillResultsAsync,
+ testAddFillResultsAsync,
+ [uint256Values, uint256Values],
+ );
+ });
+
+ describe('calculateFillResults', async () => {
+ function makeOrder(
+ makerAssetAmount: BigNumber,
+ takerAssetAmount: BigNumber,
+ makerFee: BigNumber,
+ takerFee: BigNumber,
+ ): Order {
+ return {
+ ...emptyOrder,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerFee,
+ takerFee,
+ };
+ }
+ async function referenceCalculateFillResultsAsync(
+ orderTakerAssetAmount: BigNumber,
+ takerAssetFilledAmount: BigNumber,
+ otherAmount: BigNumber,
+ ): Promise<FillResults> {
+ // Note(albrow): Here we are re-using the same value (otherAmount)
+ // for order.makerAssetAmount, order.makerFee, and order.takerFee.
+ // This should be safe because they are never used with each other
+ // in any mathematical operation in either the reference TypeScript
+ // implementation or the Solidity implementation of
+ // calculateFillResults.
+ const makerAssetFilledAmount = await referenceSafeGetPartialAmountFloorAsync(
+ takerAssetFilledAmount,
+ orderTakerAssetAmount,
+ otherAmount,
+ );
+ const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount);
+ const orderMakerAssetAmount = order.makerAssetAmount;
+ return {
+ makerAssetFilledAmount,
+ takerAssetFilledAmount,
+ makerFeePaid: await referenceSafeGetPartialAmountFloorAsync(
+ makerAssetFilledAmount,
+ orderMakerAssetAmount,
+ otherAmount,
+ ),
+ takerFeePaid: await referenceSafeGetPartialAmountFloorAsync(
+ takerAssetFilledAmount,
+ orderTakerAssetAmount,
+ otherAmount,
+ ),
+ };
+ }
+ async function testCalculateFillResultsAsync(
+ orderTakerAssetAmount: BigNumber,
+ takerAssetFilledAmount: BigNumber,
+ otherAmount: BigNumber,
+ ): Promise<FillResults> {
+ const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount);
+ return testExchange.publicCalculateFillResults.callAsync(order, takerAssetFilledAmount);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'calculateFillResults',
+ referenceCalculateFillResultsAsync,
+ testCalculateFillResultsAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('getPartialAmountFloor', async () => {
+ async function referenceGetPartialAmountFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ const product = numerator.mul(target);
+ if (product.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ return product.dividedToIntegerBy(denominator);
+ }
+ async function testGetPartialAmountFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ return testExchange.publicGetPartialAmountFloor.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'getPartialAmountFloor',
+ referenceGetPartialAmountFloorAsync,
+ testGetPartialAmountFloorAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('getPartialAmountCeil', async () => {
+ async function referenceGetPartialAmountCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ const product = numerator.mul(target);
+ const offset = product.add(denominator.sub(1));
+ if (offset.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ const result = offset.dividedToIntegerBy(denominator);
+ if (product.mod(denominator).eq(0)) {
+ expect(result.mul(denominator)).to.be.bignumber.eq(product);
+ } else {
+ expect(result.mul(denominator)).to.be.bignumber.gt(product);
+ }
+ return result;
+ }
+ async function testGetPartialAmountCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ return testExchange.publicGetPartialAmountCeil.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'getPartialAmountCeil',
+ referenceGetPartialAmountCeilAsync,
+ testGetPartialAmountCeilAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('safeGetPartialAmountFloor', async () => {
+ async function testSafeGetPartialAmountFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ return testExchange.publicSafeGetPartialAmountFloor.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'safeGetPartialAmountFloor',
+ referenceSafeGetPartialAmountFloorAsync,
+ testSafeGetPartialAmountFloorAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('safeGetPartialAmountCeil', async () => {
+ async function referenceSafeGetPartialAmountCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ if (denominator.eq(0)) {
+ throw divisionByZeroErrorForCall;
+ }
+ const isRoundingError = await referenceIsRoundingErrorCeilAsync(numerator, denominator, target);
+ if (isRoundingError) {
+ throw roundingErrorForCall;
+ }
+ const product = numerator.mul(target);
+ const offset = product.add(denominator.sub(1));
+ if (offset.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForCall;
+ }
+ const result = offset.dividedToIntegerBy(denominator);
+ if (product.mod(denominator).eq(0)) {
+ expect(result.mul(denominator)).to.be.bignumber.eq(product);
+ } else {
+ expect(result.mul(denominator)).to.be.bignumber.gt(product);
+ }
+ return result;
+ }
+ async function testSafeGetPartialAmountCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<BigNumber> {
+ return testExchange.publicSafeGetPartialAmountCeil.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'safeGetPartialAmountCeil',
+ referenceSafeGetPartialAmountCeilAsync,
+ testSafeGetPartialAmountCeilAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('isRoundingErrorFloor', async () => {
+ async function testIsRoundingErrorFloorAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<boolean> {
+ return testExchange.publicIsRoundingErrorFloor.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'isRoundingErrorFloor',
+ referenceIsRoundingErrorFloorAsync,
+ testIsRoundingErrorFloorAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('isRoundingErrorCeil', async () => {
+ async function testIsRoundingErrorCeilAsync(
+ numerator: BigNumber,
+ denominator: BigNumber,
+ target: BigNumber,
+ ): Promise<boolean> {
+ return testExchange.publicIsRoundingErrorCeil.callAsync(numerator, denominator, target);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'isRoundingErrorCeil',
+ referenceIsRoundingErrorCeilAsync,
+ testIsRoundingErrorCeilAsync,
+ [uint256Values, uint256Values, uint256Values],
+ );
+ });
+
+ describe('updateFilledState', async () => {
+ // Note(albrow): Since updateFilledState modifies the state by calling
+ // sendTransaction, we must reset the state after each test.
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ async function referenceUpdateFilledStateAsync(
+ takerAssetFilledAmount: BigNumber,
+ orderTakerAssetFilledAmount: BigNumber,
+ // tslint:disable-next-line:no-unused-variable
+ orderHash: string,
+ ): Promise<BigNumber> {
+ const totalFilledAmount = takerAssetFilledAmount.add(orderTakerAssetFilledAmount);
+ if (totalFilledAmount.greaterThan(MAX_UINT256)) {
+ throw overflowErrorForSendTransaction;
+ }
+ return totalFilledAmount;
+ }
+ async function testUpdateFilledStateAsync(
+ takerAssetFilledAmount: BigNumber,
+ orderTakerAssetFilledAmount: BigNumber,
+ orderHash: string,
+ ): Promise<BigNumber> {
+ const fillResults = {
+ makerAssetFilledAmount: new BigNumber(0),
+ takerAssetFilledAmount,
+ makerFeePaid: new BigNumber(0),
+ takerFeePaid: new BigNumber(0),
+ };
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await testExchange.publicUpdateFilledState.sendTransactionAsync(
+ emptySignedOrder,
+ constants.NULL_ADDRESS,
+ orderHash,
+ orderTakerAssetFilledAmount,
+ fillResults,
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ return testExchange.filled.callAsync(orderHash);
+ }
+ await testCombinatoriallyWithReferenceFuncAsync(
+ 'updateFilledState',
+ referenceUpdateFilledStateAsync,
+ testUpdateFilledStateAsync,
+ [uint256Values, uint256Values, bytes32Values],
+ );
+ });
+});
diff --git a/contracts/protocol/test/exchange/match_orders.ts b/contracts/protocol/test/exchange/match_orders.ts
new file mode 100644
index 000000000..8e943c237
--- /dev/null
+++ b/contracts/protocol/test/exchange/match_orders.ts
@@ -0,0 +1,1284 @@
+import {
+ chaiSetup,
+ constants,
+ ERC20BalancesByOwner,
+ ERC721TokenIdsByOwner,
+ expectTransactionFailedAsync,
+ OrderFactory,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import {
+ artifacts as tokensArtifacts,
+ DummyERC20TokenContract,
+ DummyERC721TokenContract,
+ ReentrantERC20TokenContract,
+} from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils } from '@0x/order-utils';
+import { RevertReason } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import { ExchangeContract } from '../../generated-wrappers/exchange';
+import { TestExchangeInternalsContract } from '../../generated-wrappers/test_exchange_internals';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+import { ExchangeWrapper } from '../utils/exchange_wrapper';
+import { MatchOrderTester } from '../utils/match_order_tester';
+
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('matchOrders', () => {
+ let makerAddressLeft: string;
+ let makerAddressRight: string;
+ let owner: string;
+ let takerAddress: string;
+ let feeRecipientAddressLeft: string;
+ let feeRecipientAddressRight: string;
+
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc20TokenB: DummyERC20TokenContract;
+ let zrxToken: DummyERC20TokenContract;
+ let erc721Token: DummyERC721TokenContract;
+ let reentrantErc20Token: ReentrantERC20TokenContract;
+ let exchange: ExchangeContract;
+ let erc20Proxy: ERC20ProxyContract;
+ let erc721Proxy: ERC721ProxyContract;
+
+ let erc20BalancesByOwner: ERC20BalancesByOwner;
+ let erc721TokenIdsByOwner: ERC721TokenIdsByOwner;
+ let exchangeWrapper: ExchangeWrapper;
+ let erc20Wrapper: ERC20Wrapper;
+ let erc721Wrapper: ERC721Wrapper;
+ let orderFactoryLeft: OrderFactory;
+ let orderFactoryRight: OrderFactory;
+
+ let erc721LeftMakerAssetIds: BigNumber[];
+ let erc721RightMakerAssetIds: BigNumber[];
+
+ let defaultERC20MakerAssetAddress: string;
+ let defaultERC20TakerAssetAddress: string;
+ let defaultERC721AssetAddress: string;
+
+ let matchOrderTester: MatchOrderTester;
+
+ let testExchange: TestExchangeInternalsContract;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ // Create accounts
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ // Hack(albrow): Both Prettier and TSLint insert a trailing comma below
+ // but that is invalid syntax as of TypeScript version >= 2.8. We don't
+ // have the right fine-grained configuration options in TSLint,
+ // Prettier, or TypeScript, to reconcile this, so we will just have to
+ // wait for them to sort it out. We disable TSLint and Prettier for
+ // this part of the code for now. This occurs several times in this
+ // file. See https://github.com/prettier/prettier/issues/4624.
+ // prettier-ignore
+ const usedAddresses = ([
+ owner,
+ makerAddressLeft,
+ makerAddressRight,
+ takerAddress,
+ feeRecipientAddressLeft,
+ // tslint:disable-next-line:trailing-comma
+ feeRecipientAddressRight
+ ] = _.slice(accounts, 0, 6));
+ // Create wrappers
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+ erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+ // Deploy ERC20 token & ERC20 proxy
+ const numDummyErc20ToDeploy = 3;
+ [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+ // Deploy ERC721 token and proxy
+ [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
+ erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ erc721LeftMakerAssetIds = erc721Balances[makerAddressLeft][erc721Token.address];
+ erc721RightMakerAssetIds = erc721Balances[makerAddressRight][erc721Token.address];
+ // Depoy exchange
+ exchange = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ );
+ exchangeWrapper = new ExchangeWrapper(exchange, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
+ await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner);
+ // Authorize ERC20 and ERC721 trades by exchange
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ reentrantErc20Token = await ReentrantERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.ReentrantERC20Token,
+ provider,
+ txDefaults,
+ exchange.address,
+ );
+
+ // Set default addresses
+ defaultERC20MakerAssetAddress = erc20TokenA.address;
+ defaultERC20TakerAssetAddress = erc20TokenB.address;
+ defaultERC721AssetAddress = erc721Token.address;
+ // Create default order parameters
+ const defaultOrderParamsLeft = {
+ ...constants.STATIC_ORDER_PARAMS,
+ makerAddress: makerAddressLeft,
+ exchangeAddress: exchange.address,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ };
+ const defaultOrderParamsRight = {
+ ...constants.STATIC_ORDER_PARAMS,
+ makerAddress: makerAddressRight,
+ exchangeAddress: exchange.address,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ feeRecipientAddress: feeRecipientAddressRight,
+ };
+ const privateKeyLeft = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressLeft)];
+ orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParamsLeft);
+ const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)];
+ orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParamsRight);
+ // Set match order tester
+ matchOrderTester = new MatchOrderTester(exchangeWrapper, erc20Wrapper, erc721Wrapper, zrxToken.address);
+ testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync(
+ artifacts.TestExchangeInternals,
+ provider,
+ txDefaults,
+ );
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('matchOrders', () => {
+ beforeEach(async () => {
+ erc20BalancesByOwner = await erc20Wrapper.getBalancesAsync();
+ erc721TokenIdsByOwner = await erc721Wrapper.getBalancesAsync();
+ });
+
+ it('Should transfer correct amounts when right order is fully filled and values pass isRoundingErrorFloor but fail isRoundingErrorCeil', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(17), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(98), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ // Assert is rounding error ceil & not rounding error floor
+ // These assertions are taken from MixinMatchOrders::calculateMatchedFillResults
+ // The rounding error is derived computating how much the left maker will sell.
+ const numerator = signedOrderLeft.makerAssetAmount;
+ const denominator = signedOrderLeft.takerAssetAmount;
+ const target = signedOrderRight.makerAssetAmount;
+ const isRoundingErrorCeil = await testExchange.publicIsRoundingErrorCeil.callAsync(
+ numerator,
+ denominator,
+ target,
+ );
+ expect(isRoundingErrorCeil).to.be.true();
+ const isRoundingErrorFloor = await testExchange.publicIsRoundingErrorFloor.callAsync(
+ numerator,
+ denominator,
+ target,
+ );
+ expect(isRoundingErrorFloor).to.be.false();
+ // Match signedOrderLeft with signedOrderRight
+ // Note that the left maker received a slightly better sell price.
+ // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults.
+ // Because the left maker received a slightly more favorable sell price, the fee
+ // paid by the left taker is slightly higher than that paid by the left maker.
+ // Fees can be thought of as a tax paid by the seller, derived from the sale price.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.4705882352941176'), 16), // 76.47%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.5306122448979591'), 16), // 76.53%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should transfer correct amounts when left order is fully filled and values pass isRoundingErrorCeil but fail isRoundingErrorFloor', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(15), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(97), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(14), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ // Assert is rounding error floor & not rounding error ceil
+ // These assertions are taken from MixinMatchOrders::calculateMatchedFillResults
+ // The rounding error is derived computating how much the right maker will buy.
+ const numerator = signedOrderRight.takerAssetAmount;
+ const denominator = signedOrderRight.makerAssetAmount;
+ const target = signedOrderLeft.takerAssetAmount;
+ const isRoundingErrorFloor = await testExchange.publicIsRoundingErrorFloor.callAsync(
+ numerator,
+ denominator,
+ target,
+ );
+ expect(isRoundingErrorFloor).to.be.true();
+ const isRoundingErrorCeil = await testExchange.publicIsRoundingErrorCeil.callAsync(
+ numerator,
+ denominator,
+ target,
+ );
+ expect(isRoundingErrorCeil).to.be.false();
+ // Match signedOrderLeft with signedOrderRight
+ // Note that the right maker received a slightly better purchase price.
+ // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults.
+ // Because the right maker received a slightly more favorable buy price, the fee
+ // paid by the right taker is slightly higher than that paid by the right maker.
+ // Fees can be thought of as a tax paid by the seller, derived from the sale price.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(15), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('92.7835051546391752'), 16), // 92.78%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber('92.8571428571428571'), 16), // 92.85%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should give right maker a better buy price when rounding', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(16), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(83), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(49), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ // Note:
+ // The correct price buy price for the right maker would yield (49/83) * 22 = 12.988 units
+ // of the left maker asset. This gets rounded up to 13, giving the right maker a better price.
+ // Note:
+ // The maker/taker fee percentage paid on the right order differs because
+ // they received different sale prices. The right maker pays a
+ // fee slightly lower than the right taker.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(16), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('26.5060240963855421'), 16), // 26.506%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber('26.5306122448979591'), 16), // 26.531%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should give left maker a better sell price when rounding', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(12), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(97), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ // Note:
+ // The maker/taker fee percentage paid on the left order differs because
+ // they received different sale prices. The left maker pays a fee
+ // slightly lower than the left taker.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(11), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('91.6666666666666666'), 16), // 91.6%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber('91.7525773195876288'), 16), // 91.75%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should give right maker and right taker a favorable fee price when rounding', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(16), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(83), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(49), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 0),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 0),
+ });
+ // Note:
+ // The maker/taker fee percentage paid on the right order differs because
+ // they received different sale prices. The right maker pays a
+ // fee slightly lower than the right taker.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(16), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(22), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2650), 0), // 2650.6 rounded down tro 2650
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(2653), 0), // 2653.1 rounded down to 2653
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should give left maker and left taker a favorable fee price when rounding', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(12), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(97), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 0),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 0),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ // Note:
+ // The maker/taker fee percentage paid on the left order differs because
+ // they received different sale prices. The left maker pays a
+ // fee slightly lower than the left taker.
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(11), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(9166), 0), // 9166.6 rounded down to 9166
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(89), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(9175), 0), // 9175.2 rounded down to 9175
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should transfer correct amounts when right order fill amount deviates from amount derived by `Exchange.fillOrder`', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAddress: makerAddressLeft,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1005), 0),
+ feeRecipientAddress: feeRecipientAddressLeft,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2126), 0),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1063), 0),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1005), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ // Notes:
+ // i.
+ // The left order is fully filled by the right order, so the right maker must sell 1005 units of their asset to the left maker.
+ // By selling 1005 units, the right maker should theoretically receive 502.5 units of the left maker's asset.
+ // Since the transfer amount must be an integer, this value must be rounded down to 502 or up to 503.
+ // ii.
+ // If the right order were filled via `Exchange.fillOrder` the respective fill amounts would be [1004, 502] or [1006, 503].
+ // It follows that we cannot trigger a sale of 1005 units of the right maker's asset through `Exchange.fillOrder`.
+ // iii.
+ // For an optimal match, the algorithm must choose either [1005, 502] or [1005, 503] as fill amounts for the right order.
+ // The algorithm favors the right maker when the exchange rate must be rounded, so the final fill for the right order is [1005, 503].
+ // iv.
+ // The right maker fee differs from the right taker fee because their exchange rate differs.
+ // The right maker always receives the better exchange and fee price.
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1005), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(503), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('47.2718720602069614'), 16), // 47.27%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(497), 0),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber('47.3189087488240827'), 16), // 47.31%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow matchOrders to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAddress: makerAddressRight,
+ takerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feeRecipientAddress: feeRecipientAddressRight,
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('matchOrders reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts when orders completely fill each other', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match signedOrderLeft with signedOrderRight
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts when orders completely fill each other and taker doesnt take a profit', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ });
+ // Match signedOrderLeft with signedOrderRight
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts when left order is completely filled and right order is partially filled', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(20), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 18),
+ });
+ // Match signedOrderLeft with signedOrderRight
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 16), // 50%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 16), // 50%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts when right order is completely filled and left order is partially filled', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match signedOrderLeft with signedOrderRight
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 16), // 10%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 16), // 10%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ // Match signedOrderLeft with signedOrderRight
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts when consecutive calls are used to completely fill the left order', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ let newERC20BalancesByOwner: ERC20BalancesByOwner;
+ let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 16), // 10%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 16), // 10%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ // prettier-ignore
+ [
+ newERC20BalancesByOwner,
+ // tslint:disable-next-line:trailing-comma
+ newERC721TokenIdsByOwner
+ ] = await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ // Construct second right order
+ // Note: This order needs makerAssetAmount=90/takerAssetAmount=[anything <= 45] to fully fill the right order.
+ // However, we use 100/50 to ensure a partial fill as we want to go down the "left fill"
+ // branch in the contract twice for this test.
+ const signedOrderRight2 = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18),
+ });
+ // Match signedOrderLeft with signedOrderRight2
+ const leftTakerAssetFilledAmount = signedOrderRight.makerAssetAmount;
+ const rightTakerAssetFilledAmount = new BigNumber(0);
+ const expectedTransferAmounts2 = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(45), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 16), // 90% (10% paid earlier)
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(45), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 16), // 90%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 16), // 90% (10% paid earlier)
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(90), 16), // 90%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight2,
+ takerAddress,
+ newERC20BalancesByOwner,
+ newERC721TokenIdsByOwner,
+ expectedTransferAmounts2,
+ leftTakerAssetFilledAmount,
+ rightTakerAssetFilledAmount,
+ );
+ });
+
+ it('should transfer the correct amounts when consecutive calls are used to completely fill the right order', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+ // Match orders
+ let newERC20BalancesByOwner: ERC20BalancesByOwner;
+ let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 16), // 4%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 16), // 4%
+ };
+ // prettier-ignore
+ [
+ newERC20BalancesByOwner,
+ // tslint:disable-next-line:trailing-comma
+ newERC721TokenIdsByOwner
+ ] = await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+
+ // 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 signedOrderLeft2 = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18),
+ });
+ // Match signedOrderLeft2 with signedOrderRight
+ const leftTakerAssetFilledAmount = new BigNumber(0);
+ const takerAmountReceived = newERC20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress].minus(
+ erc20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress],
+ );
+ const rightTakerAssetFilledAmount = signedOrderLeft.makerAssetAmount.minus(takerAmountReceived);
+ const expectedTransferAmounts2 = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(48), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 16), // 96%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(48), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 16), // 96%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 16), // 96%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(96), 16), // 96%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft2,
+ signedOrderRight,
+ takerAddress,
+ newERC20BalancesByOwner,
+ newERC721TokenIdsByOwner,
+ expectedTransferAmounts2,
+ leftTakerAssetFilledAmount,
+ rightTakerAssetFilledAmount,
+ );
+ });
+
+ it('should transfer the correct amounts if fee recipient is the same across both matched orders', async () => {
+ const feeRecipientAddress = feeRecipientAddressLeft;
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feeRecipientAddress,
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feeRecipientAddress,
+ });
+ // Match orders
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts if taker is also the left order maker', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ takerAddress = signedOrderLeft.makerAddress;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts if taker is also the right order maker', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ takerAddress = signedOrderRight.makerAddress;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts if taker is also the left fee recipient', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ takerAddress = feeRecipientAddressLeft;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts if taker is also the right fee recipient', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ takerAddress = feeRecipientAddressRight;
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer the correct amounts if left maker is the left fee recipient and right maker is the right fee recipient', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('Should throw if left order is not fillable', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Cancel left order
+ await exchangeWrapper.cancelOrderAsync(signedOrderLeft, signedOrderLeft.makerAddress);
+ // Match orders
+ return expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('Should throw if right order is not fillable', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Cancel right order
+ await exchangeWrapper.cancelOrderAsync(signedOrderRight, signedOrderRight.makerAddress);
+ // Match orders
+ return expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should throw if there is not a positive spread', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
+ });
+ // Match orders
+ return expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ RevertReason.NegativeSpreadRequired,
+ );
+ });
+
+ it('should throw if the left maker asset is not equal to the right taker asset ', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ return expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ // We are assuming assetData fields of the right order are the
+ // reverse of the left order, rather than checking equality. This
+ // saves a bunch of gas, but as a result if the assetData fields are
+ // off then the failure ends up happening at signature validation
+ RevertReason.InvalidOrderSignature,
+ );
+ });
+
+ it('should throw if the right maker asset is not equal to the left taker asset', async () => {
+ // Create orders to match
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ });
+ // Match orders
+ return expectTransactionFailedAsync(
+ exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress),
+ RevertReason.InvalidOrderSignature,
+ );
+ });
+
+ it('should transfer correct amounts when left order maker asset is an ERC721 token', async () => {
+ // Create orders to match
+ const erc721TokenToTransfer = erc721LeftMakerAssetIds[0];
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer),
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: new BigNumber(1),
+ });
+ // Match orders
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 50%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+
+ it('should transfer correct amounts when right order maker asset is an ERC721 token', async () => {
+ // Create orders to match
+ const erc721TokenToTransfer = erc721RightMakerAssetIds[0];
+ const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ takerAssetAmount: new BigNumber(1),
+ });
+ const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer),
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), 18),
+ });
+ // Match orders
+ const expectedTransferAmounts = {
+ // Left Maker
+ amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18),
+ amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Right Maker
+ amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 0),
+ amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), 18),
+ feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ // Taker
+ amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18),
+ feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100%
+ };
+ await matchOrderTester.matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ });
+ });
+}); // tslint:disable-line:max-file-line-count
diff --git a/contracts/protocol/test/exchange/signature_validator.ts b/contracts/protocol/test/exchange/signature_validator.ts
new file mode 100644
index 000000000..3f1786b5b
--- /dev/null
+++ b/contracts/protocol/test/exchange/signature_validator.ts
@@ -0,0 +1,526 @@
+import { artifacts as examplesArtifacts, ValidatorContract, WalletContract } from '@0x/contracts-examples';
+import {
+ addressUtils,
+ chaiSetup,
+ constants,
+ expectContractCallFailedAsync,
+ LogDecoder,
+ OrderFactory,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { artifacts as tokensArtifacts } from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils, orderHashUtils, signatureUtils } from '@0x/order-utils';
+import { RevertReason, SignatureType, SignedOrder } from '@0x/types';
+import * as chai from 'chai';
+import { LogWithDecodedArgs } from 'ethereum-types';
+import ethUtil = require('ethereumjs-util');
+
+import {
+ artifacts,
+ TestSignatureValidatorContract,
+ TestSignatureValidatorSignatureValidatorApprovalEventArgs,
+ TestStaticCallReceiverContract,
+} from '../../src';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+// tslint:disable:no-unnecessary-type-assertion
+describe('MixinSignatureValidator', () => {
+ let signedOrder: SignedOrder;
+ let orderFactory: OrderFactory;
+ let signatureValidator: TestSignatureValidatorContract;
+ let testWallet: WalletContract;
+ let testValidator: ValidatorContract;
+ let maliciousWallet: TestStaticCallReceiverContract;
+ let maliciousValidator: TestStaticCallReceiverContract;
+ let signerAddress: string;
+ let signerPrivateKey: Buffer;
+ let notSignerAddress: string;
+ let notSignerPrivateKey: Buffer;
+ let signatureValidatorLogDecoder: LogDecoder;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const makerAddress = accounts[0];
+ signerAddress = makerAddress;
+ notSignerAddress = accounts[1];
+ signatureValidator = await TestSignatureValidatorContract.deployFrom0xArtifactAsync(
+ artifacts.TestSignatureValidator,
+ provider,
+ txDefaults,
+ );
+ testWallet = await WalletContract.deployFrom0xArtifactAsync(
+ examplesArtifacts.Wallet,
+ provider,
+ txDefaults,
+ signerAddress,
+ );
+ testValidator = await ValidatorContract.deployFrom0xArtifactAsync(
+ examplesArtifacts.Validator,
+ provider,
+ txDefaults,
+ signerAddress,
+ );
+ maliciousWallet = maliciousValidator = await TestStaticCallReceiverContract.deployFrom0xArtifactAsync(
+ artifacts.TestStaticCallReceiver,
+ provider,
+ txDefaults,
+ );
+ signatureValidatorLogDecoder = new LogDecoder(web3Wrapper, { ...artifacts, ...tokensArtifacts });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(testValidator.address, true, {
+ from: signerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(
+ maliciousValidator.address,
+ true,
+ {
+ from: signerAddress,
+ },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const defaultOrderParams = {
+ ...constants.STATIC_ORDER_PARAMS,
+ exchangeAddress: signatureValidator.address,
+ makerAddress,
+ feeRecipientAddress: addressUtils.generatePseudoRandomAddress(),
+ makerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()),
+ };
+ signerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
+ notSignerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(notSignerAddress)];
+ orderFactory = new OrderFactory(signerPrivateKey, defaultOrderParams);
+ });
+
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+
+ describe('isValidSignature', () => {
+ beforeEach(async () => {
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ });
+
+ it('should revert when signature is empty', async () => {
+ const emptySignature = '0x';
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ return expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ emptySignature,
+ ),
+ RevertReason.LengthGreaterThan0Required,
+ );
+ });
+
+ it('should revert when signature type is unsupported', async () => {
+ const unsupportedSignatureType = SignatureType.NSignatureTypes;
+ const unsupportedSignatureHex = `0x${Buffer.from([unsupportedSignatureType]).toString('hex')}`;
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ return expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ unsupportedSignatureHex,
+ ),
+ RevertReason.SignatureUnsupported,
+ );
+ });
+
+ it('should revert when SignatureType=Illegal', async () => {
+ const unsupportedSignatureHex = `0x${Buffer.from([SignatureType.Illegal]).toString('hex')}`;
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ return expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ unsupportedSignatureHex,
+ ),
+ RevertReason.SignatureIllegal,
+ );
+ });
+
+ it('should return false when SignatureType=Invalid and signature has a length of zero', async () => {
+ const signatureHex = `0x${Buffer.from([SignatureType.Invalid]).toString('hex')}`;
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should revert when SignatureType=Invalid and signature length is non-zero', async () => {
+ const fillerData = ethUtil.toBuffer('0xdeadbeef');
+ const signatureType = ethUtil.toBuffer(`0x${SignatureType.Invalid}`);
+ const signatureBuffer = Buffer.concat([fillerData, signatureType]);
+ const signatureHex = ethUtil.bufferToHex(signatureBuffer);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ return expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ signatureHex,
+ ),
+ RevertReason.Length0Required,
+ );
+ });
+
+ it('should return true when SignatureType=EIP712 and signature is valid', async () => {
+ // Create EIP712 signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashBuffer = ethUtil.toBuffer(orderHashHex);
+ const ecSignature = ethUtil.ecsign(orderHashBuffer, signerPrivateKey);
+ // Create 0x signature from EIP712 signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.EIP712}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return false when SignatureType=EIP712 and signature is invalid', async () => {
+ // Create EIP712 signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashBuffer = ethUtil.toBuffer(orderHashHex);
+ const ecSignature = ethUtil.ecsign(orderHashBuffer, signerPrivateKey);
+ // Create 0x signature from EIP712 signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.EIP712}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature.
+ // This will fail because `signerAddress` signed the message, but we're passing in `notSignerAddress`
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ notSignerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should return true when SignatureType=EthSign and signature is valid', async () => {
+ // Create EthSign signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(orderHashHex);
+ const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex);
+ const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey);
+ // Create 0x signature from EthSign signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.EthSign}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return false when SignatureType=EthSign and signature is invalid', async () => {
+ // Create EthSign signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(orderHashHex);
+ const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex);
+ const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey);
+ // Create 0x signature from EthSign signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.EthSign}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature.
+ // This will fail because `signerAddress` signed the message, but we're passing in `notSignerAddress`
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ notSignerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should return true when SignatureType=Wallet and signature is valid', async () => {
+ // Create EIP712 signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashBuffer = ethUtil.toBuffer(orderHashHex);
+ const ecSignature = ethUtil.ecsign(orderHashBuffer, signerPrivateKey);
+ // Create 0x signature from EIP712 signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.Wallet}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ testWallet.address,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return false when SignatureType=Wallet and signature is invalid', async () => {
+ // Create EIP712 signature using a private key that does not belong to the wallet owner.
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashBuffer = ethUtil.toBuffer(orderHashHex);
+ const notWalletOwnerPrivateKey = notSignerPrivateKey;
+ const ecSignature = ethUtil.ecsign(orderHashBuffer, notWalletOwnerPrivateKey);
+ // Create 0x signature from EIP712 signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.Wallet}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ // Validate signature
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ testWallet.address,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should revert when `isValidSignature` attempts to update state and SignatureType=Wallet', async () => {
+ // Create EIP712 signature
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const orderHashBuffer = ethUtil.toBuffer(orderHashHex);
+ const ecSignature = ethUtil.ecsign(orderHashBuffer, signerPrivateKey);
+ // Create 0x signature from EIP712 signature
+ const signature = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ecSignature.r,
+ ecSignature.s,
+ ethUtil.toBuffer(`0x${SignatureType.Wallet}`),
+ ]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ await expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ maliciousWallet.address,
+ signatureHex,
+ ),
+ RevertReason.WalletError,
+ );
+ });
+
+ it('should return true when SignatureType=Validator, signature is valid and validator is approved', async () => {
+ const validatorAddress = ethUtil.toBuffer(`${testValidator.address}`);
+ const signatureType = ethUtil.toBuffer(`0x${SignatureType.Validator}`);
+ const signature = Buffer.concat([validatorAddress, signatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return false when SignatureType=Validator, signature is invalid and validator is approved', async () => {
+ const validatorAddress = ethUtil.toBuffer(`${testValidator.address}`);
+ const signatureType = ethUtil.toBuffer(`0x${SignatureType.Validator}`);
+ const signature = Buffer.concat([validatorAddress, signatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ // This will return false because we signed the message with `signerAddress`, but
+ // are validating against `notSignerAddress`
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ notSignerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should revert when `isValidSignature` attempts to update state and SignatureType=Validator', async () => {
+ const validatorAddress = ethUtil.toBuffer(`${maliciousValidator.address}`);
+ const signatureType = ethUtil.toBuffer(`0x${SignatureType.Validator}`);
+ const signature = Buffer.concat([validatorAddress, signatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ await expectContractCallFailedAsync(
+ signatureValidator.publicIsValidSignature.callAsync(orderHashHex, signerAddress, signatureHex),
+ RevertReason.ValidatorError,
+ );
+ });
+ it('should return false when SignatureType=Validator, signature is valid and validator is not approved', async () => {
+ // Set approval of signature validator to false
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(
+ testValidator.address,
+ false,
+ { from: signerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Validate signature
+ const validatorAddress = ethUtil.toBuffer(`${testValidator.address}`);
+ const signatureType = ethUtil.toBuffer(`0x${SignatureType.Validator}`);
+ const signature = Buffer.concat([validatorAddress, signatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should return true when SignatureType=Presigned and signer has presigned hash', async () => {
+ // Presign hash
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await signatureValidator.preSign.sendTransactionAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ signedOrder.signature,
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ // Validate presigned signature
+ const signature = ethUtil.toBuffer(`0x${SignatureType.PreSigned}`);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return false when SignatureType=Presigned and signer has not presigned hash', async () => {
+ const signature = ethUtil.toBuffer(`0x${SignatureType.PreSigned}`);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ orderHashHex,
+ signedOrder.makerAddress,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.false();
+ });
+
+ it('should return true when message was signed by a Trezor One (firmware version 1.6.2)', async () => {
+ // messageHash translates to 0x2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b
+ const messageHash = ethUtil.bufferToHex(ethUtil.toBuffer('++++++++++++++++++++++++++++++++'));
+ const signer = '0xc28b145f10f0bcf0fc000e778615f8fd73490bad';
+ const v = ethUtil.toBuffer('0x1c');
+ const r = ethUtil.toBuffer('0x7b888b596ccf87f0bacab0dcb483124973f7420f169b4824d7a12534ac1e9832');
+ const s = ethUtil.toBuffer('0x0c8e14f7edc01459e13965f1da56e0c23ed11e2cca932571eee1292178f90424');
+ const trezorSignatureType = ethUtil.toBuffer(`0x${SignatureType.EthSign}`);
+ const signature = Buffer.concat([v, r, s, trezorSignatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ messageHash,
+ signer,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+
+ it('should return true when message was signed by a Trezor Model T (firmware version 2.0.7)', async () => {
+ // messageHash translates to 0x2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b
+ const messageHash = ethUtil.bufferToHex(ethUtil.toBuffer('++++++++++++++++++++++++++++++++'));
+ const signer = '0x98ce6d9345e8ffa7d99ee0822272fae9d2c0e895';
+ const v = ethUtil.toBuffer('0x1c');
+ const r = ethUtil.toBuffer('0x423b71062c327f0ec4fe199b8da0f34185e59b4c1cb4cc23df86cac4a601fb3f');
+ const s = ethUtil.toBuffer('0x53810d6591b5348b7ee08ee812c874b0fdfb942c9849d59512c90e295221091f');
+ const trezorSignatureType = ethUtil.toBuffer(`0x${SignatureType.EthSign}`);
+ const signature = Buffer.concat([v, r, s, trezorSignatureType]);
+ const signatureHex = ethUtil.bufferToHex(signature);
+ const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync(
+ messageHash,
+ signer,
+ signatureHex,
+ );
+ expect(isValidSignature).to.be.true();
+ });
+ });
+
+ describe('setSignatureValidatorApproval', () => {
+ it('should emit a SignatureValidatorApprovalSet with correct args when a validator is approved', async () => {
+ const approval = true;
+ const res = await signatureValidatorLogDecoder.getTxWithDecodedLogsAsync(
+ await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(
+ testValidator.address,
+ approval,
+ {
+ from: signerAddress,
+ },
+ ),
+ );
+ expect(res.logs.length).to.equal(1);
+ const log = res.logs[0] as LogWithDecodedArgs<TestSignatureValidatorSignatureValidatorApprovalEventArgs>;
+ const logArgs = log.args;
+ expect(logArgs.signerAddress).to.equal(signerAddress);
+ expect(logArgs.validatorAddress).to.equal(testValidator.address);
+ expect(logArgs.approved).to.equal(approval);
+ });
+ it('should emit a SignatureValidatorApprovalSet with correct args when a validator is disapproved', async () => {
+ const approval = false;
+ const res = await signatureValidatorLogDecoder.getTxWithDecodedLogsAsync(
+ await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(
+ testValidator.address,
+ approval,
+ {
+ from: signerAddress,
+ },
+ ),
+ );
+ expect(res.logs.length).to.equal(1);
+ const log = res.logs[0] as LogWithDecodedArgs<TestSignatureValidatorSignatureValidatorApprovalEventArgs>;
+ const logArgs = log.args;
+ expect(logArgs.signerAddress).to.equal(signerAddress);
+ expect(logArgs.validatorAddress).to.equal(testValidator.address);
+ expect(logArgs.approved).to.equal(approval);
+ });
+ });
+});
+// tslint:disable:max-file-line-count
+// tslint:enable:no-unnecessary-type-assertion
diff --git a/contracts/protocol/test/exchange/transactions.ts b/contracts/protocol/test/exchange/transactions.ts
new file mode 100644
index 000000000..746f3cb04
--- /dev/null
+++ b/contracts/protocol/test/exchange/transactions.ts
@@ -0,0 +1,466 @@
+import { artifacts as examplesArtifacts, ExchangeWrapperContract, WhitelistContract } from '@0x/contracts-examples';
+import {
+ chaiSetup,
+ constants,
+ ERC20BalancesByOwner,
+ expectTransactionFailedAsync,
+ OrderFactory,
+ orderUtils,
+ provider,
+ SignedTransaction,
+ TransactionFactory,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { DummyERC20TokenContract } from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
+import { OrderWithoutExchangeAddress, RevertReason, SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ExchangeContract } from '../../generated-wrappers/exchange';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ExchangeWrapper } from '../utils/exchange_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('Exchange transactions', () => {
+ let senderAddress: string;
+ let owner: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ let feeRecipientAddress: string;
+
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc20TokenB: DummyERC20TokenContract;
+ let zrxToken: DummyERC20TokenContract;
+ let exchange: ExchangeContract;
+ let erc20Proxy: ERC20ProxyContract;
+
+ let erc20Balances: ERC20BalancesByOwner;
+ let signedOrder: SignedOrder;
+ let signedTx: SignedTransaction;
+ let orderWithoutExchangeAddress: OrderWithoutExchangeAddress;
+ let orderFactory: OrderFactory;
+ let makerTransactionFactory: TransactionFactory;
+ let takerTransactionFactory: TransactionFactory;
+ let exchangeWrapper: ExchangeWrapper;
+ let erc20Wrapper: ERC20Wrapper;
+
+ let defaultMakerTokenAddress: string;
+ let defaultTakerTokenAddress: string;
+ let makerPrivateKey: Buffer;
+ let takerPrivateKey: Buffer;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, senderAddress, makerAddress, takerAddress, feeRecipientAddress] = _.slice(
+ accounts,
+ 0,
+ 5,
+ ));
+
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+
+ const numDummyErc20ToDeploy = 3;
+ [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ exchange = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ );
+ exchangeWrapper = new ExchangeWrapper(exchange, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
+
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ defaultMakerTokenAddress = erc20TokenA.address;
+ defaultTakerTokenAddress = erc20TokenB.address;
+
+ const defaultOrderParams = {
+ ...constants.STATIC_ORDER_PARAMS,
+ senderAddress,
+ exchangeAddress: exchange.address,
+ makerAddress,
+ feeRecipientAddress,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerTokenAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerTokenAddress),
+ };
+ makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
+ takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)];
+ orderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams);
+ makerTransactionFactory = new TransactionFactory(makerPrivateKey, exchange.address);
+ takerTransactionFactory = new TransactionFactory(takerPrivateKey, exchange.address);
+ });
+ describe('executeTransaction', () => {
+ describe('fillOrder', () => {
+ let takerAssetFillAmount: BigNumber;
+ beforeEach(async () => {
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ signedOrder = await orderFactory.newSignedOrderAsync();
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+
+ takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ const data = exchange.fillOrder.getABIEncodedTransactionData(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ signedOrder.signature,
+ );
+ signedTx = takerTransactionFactory.newSignedTransaction(data);
+ });
+
+ it('should throw if not called by specified sender', async () => {
+ return expectTransactionFailedAsync(
+ exchangeWrapper.executeTransactionAsync(signedTx, takerAddress),
+ RevertReason.FailedExecution,
+ );
+ });
+
+ it('should transfer the correct amounts when signed by taker and called by sender', async () => {
+ await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFillAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFeePaid = signedOrder.makerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFeePaid = signedOrder.takerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
+ );
+ });
+
+ it('should throw if the a 0x transaction with the same transactionHash has already been executed', async () => {
+ await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.executeTransactionAsync(signedTx, senderAddress),
+ RevertReason.InvalidTxHash,
+ );
+ });
+
+ it('should reset the currentContextAddress', async () => {
+ await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress);
+ const currentContextAddress = await exchange.currentContextAddress.callAsync();
+ expect(currentContextAddress).to.equal(constants.NULL_ADDRESS);
+ });
+ });
+
+ describe('cancelOrder', () => {
+ beforeEach(async () => {
+ const data = exchange.cancelOrder.getABIEncodedTransactionData(orderWithoutExchangeAddress);
+ signedTx = makerTransactionFactory.newSignedTransaction(data);
+ });
+
+ it('should throw if not called by specified sender', async () => {
+ return expectTransactionFailedAsync(
+ exchangeWrapper.executeTransactionAsync(signedTx, makerAddress),
+ RevertReason.FailedExecution,
+ );
+ });
+
+ it('should cancel the order when signed by maker and called by sender', async () => {
+ await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress);
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrderAsync(signedOrder, senderAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+ });
+
+ describe('cancelOrdersUpTo', () => {
+ let exchangeWrapperContract: ExchangeWrapperContract;
+
+ before(async () => {
+ exchangeWrapperContract = await ExchangeWrapperContract.deployFrom0xArtifactAsync(
+ examplesArtifacts.ExchangeWrapper,
+ provider,
+ txDefaults,
+ exchange.address,
+ );
+ });
+
+ it("should cancel an order if called from the order's sender", async () => {
+ const orderSalt = new BigNumber(0);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ senderAddress: exchangeWrapperContract.address,
+ salt: orderSalt,
+ });
+ const targetOrderEpoch = orderSalt.add(1);
+ const cancelData = exchange.cancelOrdersUpTo.getABIEncodedTransactionData(targetOrderEpoch);
+ const signedCancelTx = makerTransactionFactory.newSignedTransaction(cancelData);
+ await exchangeWrapperContract.cancelOrdersUpTo.sendTransactionAsync(
+ targetOrderEpoch,
+ signedCancelTx.salt,
+ signedCancelTx.signature,
+ {
+ from: makerAddress,
+ },
+ );
+
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+ const fillData = exchange.fillOrder.getABIEncodedTransactionData(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ signedOrder.signature,
+ );
+ const signedFillTx = takerTransactionFactory.newSignedTransaction(fillData);
+ return expectTransactionFailedAsync(
+ exchangeWrapperContract.fillOrder.sendTransactionAsync(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ signedFillTx.salt,
+ signedOrder.signature,
+ signedFillTx.signature,
+ { from: takerAddress },
+ ),
+ RevertReason.FailedExecution,
+ );
+ });
+
+ it("should not cancel an order if not called from the order's sender", async () => {
+ const orderSalt = new BigNumber(0);
+ signedOrder = await orderFactory.newSignedOrderAsync({
+ senderAddress: exchangeWrapperContract.address,
+ salt: orderSalt,
+ });
+ const targetOrderEpoch = orderSalt.add(1);
+ await exchangeWrapper.cancelOrdersUpToAsync(targetOrderEpoch, makerAddress);
+
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+ const data = exchange.fillOrder.getABIEncodedTransactionData(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ signedOrder.signature,
+ );
+ signedTx = takerTransactionFactory.newSignedTransaction(data);
+ await exchangeWrapperContract.fillOrder.sendTransactionAsync(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ signedTx.salt,
+ signedOrder.signature,
+ signedTx.signature,
+ { from: takerAddress },
+ );
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFillAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFeePaid = signedOrder.makerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFeePaid = signedOrder.takerFee
+ .times(makerAssetFillAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
+ );
+ });
+ });
+ });
+
+ describe('Whitelist', () => {
+ let whitelist: WhitelistContract;
+ let whitelistOrderFactory: OrderFactory;
+
+ before(async () => {
+ whitelist = await WhitelistContract.deployFrom0xArtifactAsync(
+ examplesArtifacts.Whitelist,
+ provider,
+ txDefaults,
+ exchange.address,
+ );
+ const isApproved = true;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await exchange.setSignatureValidatorApproval.sendTransactionAsync(whitelist.address, isApproved, {
+ from: takerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ const defaultOrderParams = {
+ ...constants.STATIC_ORDER_PARAMS,
+ senderAddress: whitelist.address,
+ exchangeAddress: exchange.address,
+ makerAddress,
+ feeRecipientAddress,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerTokenAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerTokenAddress),
+ };
+ whitelistOrderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams);
+ });
+
+ beforeEach(async () => {
+ signedOrder = await whitelistOrderFactory.newSignedOrderAsync();
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ });
+
+ it('should revert if maker has not been whitelisted', async () => {
+ const isApproved = true;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await whitelist.updateWhitelistStatus.sendTransactionAsync(takerAddress, isApproved, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ const salt = generatePseudoRandomSalt();
+ return expectTransactionFailedAsync(
+ whitelist.fillOrderIfWhitelisted.sendTransactionAsync(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ salt,
+ signedOrder.signature,
+ { from: takerAddress },
+ ),
+ RevertReason.MakerNotWhitelisted,
+ );
+ });
+
+ it('should revert if taker has not been whitelisted', async () => {
+ const isApproved = true;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await whitelist.updateWhitelistStatus.sendTransactionAsync(makerAddress, isApproved, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ const salt = generatePseudoRandomSalt();
+ return expectTransactionFailedAsync(
+ whitelist.fillOrderIfWhitelisted.sendTransactionAsync(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ salt,
+ signedOrder.signature,
+ { from: takerAddress },
+ ),
+ RevertReason.TakerNotWhitelisted,
+ );
+ });
+
+ it('should fill the order if maker and taker have been whitelisted', async () => {
+ const isApproved = true;
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await whitelist.updateWhitelistStatus.sendTransactionAsync(makerAddress, isApproved, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await whitelist.updateWhitelistStatus.sendTransactionAsync(takerAddress, isApproved, { from: owner }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ const salt = generatePseudoRandomSalt();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await whitelist.fillOrderIfWhitelisted.sendTransactionAsync(
+ orderWithoutExchangeAddress,
+ takerAssetFillAmount,
+ salt,
+ signedOrder.signature,
+ { from: takerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAssetFillAmount = signedOrder.makerAssetAmount;
+ const makerFeePaid = signedOrder.makerFee;
+ const takerFeePaid = signedOrder.takerFee;
+
+ expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
+ );
+ expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
+ );
+ });
+ });
+});
diff --git a/contracts/protocol/test/exchange/wrapper.ts b/contracts/protocol/test/exchange/wrapper.ts
new file mode 100644
index 000000000..c9efb0c9d
--- /dev/null
+++ b/contracts/protocol/test/exchange/wrapper.ts
@@ -0,0 +1,1461 @@
+import {
+ chaiSetup,
+ constants,
+ ERC20BalancesByOwner,
+ expectTransactionFailedAsync,
+ getLatestBlockTimestampAsync,
+ increaseTimeAndMineBlockAsync,
+ OrderFactory,
+ OrderStatus,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import {
+ artifacts as tokensArtifacts,
+ DummyERC20TokenContract,
+ DummyERC721TokenContract,
+ ReentrantERC20TokenContract,
+} from '@0x/contracts-tokens';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils, orderHashUtils } from '@0x/order-utils';
+import { RevertReason, SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import { ExchangeContract } from '../../generated-wrappers/exchange';
+import { artifacts } from '../../src/artifacts';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+import { ExchangeWrapper } from '../utils/exchange_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('Exchange wrappers', () => {
+ let makerAddress: string;
+ let owner: string;
+ let takerAddress: string;
+ let feeRecipientAddress: string;
+
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc20TokenB: DummyERC20TokenContract;
+ let zrxToken: DummyERC20TokenContract;
+ let erc721Token: DummyERC721TokenContract;
+ let exchange: ExchangeContract;
+ let erc20Proxy: ERC20ProxyContract;
+ let erc721Proxy: ERC721ProxyContract;
+ let reentrantErc20Token: ReentrantERC20TokenContract;
+
+ let exchangeWrapper: ExchangeWrapper;
+ let erc20Wrapper: ERC20Wrapper;
+ let erc721Wrapper: ERC721Wrapper;
+ let erc20Balances: ERC20BalancesByOwner;
+ let orderFactory: OrderFactory;
+
+ let erc721MakerAssetId: BigNumber;
+ let erc721TakerAssetId: BigNumber;
+
+ let defaultMakerAssetAddress: string;
+ let defaultTakerAssetAddress: string;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = _.slice(accounts, 0, 4));
+
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+ erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+
+ const numDummyErc20ToDeploy = 3;
+ [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
+ erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ erc721MakerAssetId = erc721Balances[makerAddress][erc721Token.address][0];
+ erc721TakerAssetId = erc721Balances[takerAddress][erc721Token.address][0];
+
+ exchange = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ );
+ exchangeWrapper = new ExchangeWrapper(exchange, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
+ await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner);
+
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, {
+ from: owner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ reentrantErc20Token = await ReentrantERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.ReentrantERC20Token,
+ provider,
+ txDefaults,
+ exchange.address,
+ );
+
+ defaultMakerAssetAddress = erc20TokenA.address;
+ defaultTakerAssetAddress = erc20TokenB.address;
+
+ const defaultOrderParams = {
+ ...constants.STATIC_ORDER_PARAMS,
+ exchangeAddress: exchange.address,
+ makerAddress,
+ feeRecipientAddress,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress),
+ };
+ const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
+ orderFactory = new OrderFactory(privateKey, defaultOrderParams);
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('fillOrKillOrder', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow fillOrKillOrder to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('fillOrKillOrder reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
+ });
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ await exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should throw if a signedOrder is expired', async () => {
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ expirationTimeSeconds: new BigNumber(currentTimestamp).sub(10),
+ });
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress),
+ RevertReason.OrderUnfillable,
+ );
+ });
+
+ it('should throw if entire takerAssetFillAmount not filled', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync();
+
+ await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount: signedOrder.takerAssetAmount.div(2),
+ });
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress),
+ RevertReason.CompleteFillFailed,
+ );
+ });
+ });
+
+ describe('fillOrderNoThrow', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow fillOrderNoThrow to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(erc20Balances).to.deep.equal(newBalances);
+ });
+ });
+ };
+ describe('fillOrderNoThrow reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
+ });
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 250000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should not change erc20Balances if maker erc20Balances are too low to fill order', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18),
+ });
+
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if taker erc20Balances are too low to fill order', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18),
+ });
+
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if maker allowances are too low to fill order', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, new BigNumber(0), {
+ from: makerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, constants.INITIAL_ERC20_ALLOWANCE, {
+ from: makerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if taker allowances are too low to fill order', async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync();
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenB.approve.sendTransactionAsync(erc20Proxy.address, new BigNumber(0), {
+ from: takerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20TokenB.approve.sendTransactionAsync(erc20Proxy.address, constants.INITIAL_ERC20_ALLOWANCE, {
+ from: takerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if makerAssetAddress is ZRX, makerAssetAmount + makerFee > maker balance', async () => {
+ const makerZRXBalance = new BigNumber(erc20Balances[makerAddress][zrxToken.address]);
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: makerZRXBalance,
+ makerFee: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ });
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if makerAssetAddress is ZRX, makerAssetAmount + makerFee > maker allowance', async () => {
+ const makerZRXAllowance = await zrxToken.allowance.callAsync(makerAddress, erc20Proxy.address);
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(makerZRXAllowance),
+ makerFee: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ });
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if takerAssetAddress is ZRX, takerAssetAmount + takerFee > taker balance', async () => {
+ const takerZRXBalance = new BigNumber(erc20Balances[takerAddress][zrxToken.address]);
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ takerAssetAmount: takerZRXBalance,
+ takerFee: new BigNumber(1),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ });
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not change erc20Balances if takerAssetAddress is ZRX, takerAssetAmount + takerFee > taker allowance', async () => {
+ const takerZRXAllowance = await zrxToken.allowance.callAsync(takerAddress, erc20Proxy.address);
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ takerAssetAmount: new BigNumber(takerZRXAllowance),
+ takerFee: new BigNumber(1),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ });
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should successfully exchange ERC721 tokens', async () => {
+ // Construct Exchange parameters
+ const makerAssetId = erc721MakerAssetId;
+ const takerAssetId = erc721TakerAssetId;
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ takerAssetAmount: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId),
+ });
+ // Verify pre-conditions
+ const initialOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(initialOwnerMakerAsset).to.be.bignumber.equal(makerAddress);
+ const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ // Call Exchange
+ const takerAssetFillAmount = signedOrder.takerAssetAmount;
+ await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress, {
+ takerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 280000,
+ });
+ // Verify post-conditions
+ const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress);
+ const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId);
+ expect(newOwnerTakerAsset).to.be.bignumber.equal(makerAddress);
+ });
+ });
+
+ describe('batch functions', () => {
+ let signedOrders: SignedOrder[];
+ beforeEach(async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ ];
+ });
+
+ describe('batchFillOrders', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow batchFillOrders to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.batchFillOrdersAsync([signedOrder], takerAddress),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('batchFillOrders reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts', async () => {
+ const takerAssetFillAmounts: BigNumber[] = [];
+ const makerAssetAddress = erc20TokenA.address;
+ const takerAssetAddress = erc20TokenB.address;
+ _.forEach(signedOrders, signedOrder => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ takerAssetFillAmounts.push(takerAssetFillAmount);
+ erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][
+ makerAssetAddress
+ ].minus(makerAssetFilledAmount);
+ erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add(
+ takerAssetFillAmount,
+ );
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ makerFee,
+ );
+ erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add(
+ makerAssetFilledAmount,
+ );
+ erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][
+ takerAssetAddress
+ ].minus(takerAssetFillAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(makerFee.add(takerFee));
+ });
+
+ await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmounts,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+ });
+
+ describe('batchFillOrKillOrders', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow batchFillOrKillOrders to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.batchFillOrKillOrdersAsync([signedOrder], takerAddress),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('batchFillOrKillOrders reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts', async () => {
+ const takerAssetFillAmounts: BigNumber[] = [];
+ const makerAssetAddress = erc20TokenA.address;
+ const takerAssetAddress = erc20TokenB.address;
+ _.forEach(signedOrders, signedOrder => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ takerAssetFillAmounts.push(takerAssetFillAmount);
+ erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][
+ makerAssetAddress
+ ].minus(makerAssetFilledAmount);
+ erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add(
+ takerAssetFillAmount,
+ );
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ makerFee,
+ );
+ erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add(
+ makerAssetFilledAmount,
+ );
+ erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][
+ takerAssetAddress
+ ].minus(takerAssetFillAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(makerFee.add(takerFee));
+ });
+
+ await exchangeWrapper.batchFillOrKillOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmounts,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should throw if a single signedOrder does not fill the expected amount', async () => {
+ const takerAssetFillAmounts: BigNumber[] = [];
+ _.forEach(signedOrders, signedOrder => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ takerAssetFillAmounts.push(takerAssetFillAmount);
+ });
+
+ await exchangeWrapper.fillOrKillOrderAsync(signedOrders[0], takerAddress);
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.batchFillOrKillOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmounts,
+ }),
+ RevertReason.OrderUnfillable,
+ );
+ });
+ });
+
+ describe('batchFillOrdersNoThrow', async () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow batchFillOrdersNoThrow to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.batchFillOrdersNoThrowAsync([signedOrder], takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(erc20Balances).to.deep.equal(newBalances);
+ });
+ });
+ };
+ describe('batchFillOrdersNoThrow reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should transfer the correct amounts', async () => {
+ const takerAssetFillAmounts: BigNumber[] = [];
+ const makerAssetAddress = erc20TokenA.address;
+ const takerAssetAddress = erc20TokenB.address;
+ _.forEach(signedOrders, signedOrder => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ takerAssetFillAmounts.push(takerAssetFillAmount);
+ erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][
+ makerAssetAddress
+ ].minus(makerAssetFilledAmount);
+ erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add(
+ takerAssetFillAmount,
+ );
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ makerFee,
+ );
+ erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add(
+ makerAssetFilledAmount,
+ );
+ erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][
+ takerAssetAddress
+ ].minus(takerAssetFillAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(makerFee.add(takerFee));
+ });
+
+ await exchangeWrapper.batchFillOrdersNoThrowAsync(signedOrders, takerAddress, {
+ takerAssetFillAmounts,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not throw if an order is invalid and fill the remaining orders', async () => {
+ const takerAssetFillAmounts: BigNumber[] = [];
+ const makerAssetAddress = erc20TokenA.address;
+ const takerAssetAddress = erc20TokenB.address;
+
+ const invalidOrder = {
+ ...signedOrders[0],
+ signature: '0x00',
+ };
+ const validOrders = signedOrders.slice(1);
+
+ takerAssetFillAmounts.push(invalidOrder.takerAssetAmount.div(2));
+ _.forEach(validOrders, signedOrder => {
+ const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2);
+ const makerAssetFilledAmount = takerAssetFillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+ const makerFee = signedOrder.makerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ const takerFee = signedOrder.takerFee
+ .times(makerAssetFilledAmount)
+ .dividedToIntegerBy(signedOrder.makerAssetAmount);
+ takerAssetFillAmounts.push(takerAssetFillAmount);
+ erc20Balances[makerAddress][makerAssetAddress] = erc20Balances[makerAddress][
+ makerAssetAddress
+ ].minus(makerAssetFilledAmount);
+ erc20Balances[makerAddress][takerAssetAddress] = erc20Balances[makerAddress][takerAssetAddress].add(
+ takerAssetFillAmount,
+ );
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ makerFee,
+ );
+ erc20Balances[takerAddress][makerAssetAddress] = erc20Balances[takerAddress][makerAssetAddress].add(
+ makerAssetFilledAmount,
+ );
+ erc20Balances[takerAddress][takerAssetAddress] = erc20Balances[takerAddress][
+ takerAssetAddress
+ ].minus(takerAssetFillAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(makerFee.add(takerFee));
+ });
+
+ const newOrders = [invalidOrder, ...validOrders];
+ await exchangeWrapper.batchFillOrdersNoThrowAsync(newOrders, takerAddress, {
+ takerAssetFillAmounts,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 450000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+ });
+
+ describe('marketSellOrders', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow marketSellOrders to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.marketSellOrdersAsync([signedOrder], takerAddress, {
+ takerAssetFillAmount: signedOrder.takerAssetAmount,
+ }),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('marketSellOrders reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should stop when the entire takerAssetFillAmount is filled', async () => {
+ const takerAssetFillAmount = signedOrders[0].takerAssetAmount.plus(
+ signedOrders[1].takerAssetAmount.div(2),
+ );
+ await exchangeWrapper.marketSellOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAssetFilledAmount = signedOrders[0].makerAssetAmount.add(
+ signedOrders[1].makerAssetAmount.dividedToIntegerBy(2),
+ );
+ const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2));
+ const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should fill all signedOrders if cannot fill entire takerAssetFillAmount', async () => {
+ const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ _.forEach(signedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketSellOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should throw when a signedOrder does not use the same takerAssetAddress', async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ }),
+ await orderFactory.newSignedOrderAsync(),
+ ];
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.marketSellOrdersAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), 18),
+ }),
+ // We simply use the takerAssetData from the first order for all orders.
+ // If they are not the same, the contract throws when validating the order signature
+ RevertReason.InvalidOrderSignature,
+ );
+ });
+ });
+
+ describe('marketSellOrdersNoThrow', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow marketSellOrdersNoThrow to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.marketSellOrdersNoThrowAsync([signedOrder], takerAddress, {
+ takerAssetFillAmount: signedOrder.takerAssetAmount,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(erc20Balances).to.deep.equal(newBalances);
+ });
+ });
+ };
+ describe('marketSellOrdersNoThrow reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should stop when the entire takerAssetFillAmount is filled', async () => {
+ const takerAssetFillAmount = signedOrders[0].takerAssetAmount.plus(
+ signedOrders[1].takerAssetAmount.div(2),
+ );
+ await exchangeWrapper.marketSellOrdersNoThrowAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 6000000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAssetFilledAmount = signedOrders[0].makerAssetAmount.add(
+ signedOrders[1].makerAssetAmount.dividedToIntegerBy(2),
+ );
+ const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2));
+ const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should fill all signedOrders if cannot fill entire takerAssetFillAmount', async () => {
+ const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ _.forEach(signedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketSellOrdersNoThrowAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not fill a signedOrder that does not use the same takerAssetAddress', async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync({
+ takerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ }),
+ ];
+ const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ const filledSignedOrders = signedOrders.slice(0, -1);
+ _.forEach(filledSignedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketSellOrdersNoThrowAsync(signedOrders, takerAddress, {
+ takerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+ });
+
+ describe('marketBuyOrders', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow marketBuyOrders to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await expectTransactionFailedAsync(
+ exchangeWrapper.marketBuyOrdersAsync([signedOrder], takerAddress, {
+ makerAssetFillAmount: signedOrder.makerAssetAmount,
+ }),
+ RevertReason.TransferFailed,
+ );
+ });
+ });
+ };
+ describe('marketBuyOrders reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should stop when the entire makerAssetFillAmount is filled', async () => {
+ const makerAssetFillAmount = signedOrders[0].makerAssetAmount.plus(
+ signedOrders[1].makerAssetAmount.div(2),
+ );
+ await exchangeWrapper.marketBuyOrdersAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAmountBought = signedOrders[0].takerAssetAmount.add(
+ signedOrders[1].takerAssetAmount.dividedToIntegerBy(2),
+ );
+ const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2));
+ const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(makerAmountBought),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(makerAmountBought),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should fill all signedOrders if cannot fill entire makerAssetFillAmount', async () => {
+ const makerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ _.forEach(signedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketBuyOrdersAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should throw when a signedOrder does not use the same makerAssetAddress', async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ }),
+ await orderFactory.newSignedOrderAsync(),
+ ];
+
+ return expectTransactionFailedAsync(
+ exchangeWrapper.marketBuyOrdersAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), 18),
+ }),
+ RevertReason.InvalidOrderSignature,
+ );
+ });
+ });
+
+ describe('marketBuyOrdersNoThrow', () => {
+ const reentrancyTest = (functionNames: string[]) => {
+ _.forEach(functionNames, async (functionName: string, functionId: number) => {
+ const description = `should not allow marketBuyOrdersNoThrow to reenter the Exchange contract via ${functionName}`;
+ it(description, async () => {
+ const signedOrder = await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address),
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await reentrantErc20Token.setCurrentFunction.sendTransactionAsync(functionId),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await exchangeWrapper.marketBuyOrdersNoThrowAsync([signedOrder], takerAddress, {
+ makerAssetFillAmount: signedOrder.makerAssetAmount,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(erc20Balances).to.deep.equal(newBalances);
+ });
+ });
+ };
+ describe('marketBuyOrdersNoThrow reentrancy tests', () => reentrancyTest(constants.FUNCTIONS_WITH_MUTEX));
+
+ it('should stop when the entire makerAssetFillAmount is filled', async () => {
+ const makerAssetFillAmount = signedOrders[0].makerAssetAmount.plus(
+ signedOrders[1].makerAssetAmount.div(2),
+ );
+ await exchangeWrapper.marketBuyOrdersNoThrowAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+
+ const makerAmountBought = signedOrders[0].takerAssetAmount.add(
+ signedOrders[1].takerAssetAmount.dividedToIntegerBy(2),
+ );
+ const makerFee = signedOrders[0].makerFee.add(signedOrders[1].makerFee.dividedToIntegerBy(2));
+ const takerFee = signedOrders[0].takerFee.add(signedOrders[1].takerFee.dividedToIntegerBy(2));
+ expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount),
+ );
+ expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][defaultTakerAssetAddress].add(makerAmountBought),
+ );
+ expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][zrxToken.address].minus(makerFee),
+ );
+ expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultTakerAssetAddress].minus(makerAmountBought),
+ );
+ expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount),
+ );
+ expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[takerAddress][zrxToken.address].minus(takerFee),
+ );
+ expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
+ erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)),
+ );
+ });
+
+ it('should fill all signedOrders if cannot fill entire makerAssetFillAmount', async () => {
+ const makerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ _.forEach(signedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketBuyOrdersNoThrowAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+
+ it('should not fill a signedOrder that does not use the same makerAssetAddress', async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address),
+ }),
+ ];
+
+ const makerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18);
+ const filledSignedOrders = signedOrders.slice(0, -1);
+ _.forEach(filledSignedOrders, signedOrder => {
+ erc20Balances[makerAddress][defaultMakerAssetAddress] = erc20Balances[makerAddress][
+ defaultMakerAssetAddress
+ ].minus(signedOrder.makerAssetAmount);
+ erc20Balances[makerAddress][defaultTakerAssetAddress] = erc20Balances[makerAddress][
+ defaultTakerAssetAddress
+ ].add(signedOrder.takerAssetAmount);
+ erc20Balances[makerAddress][zrxToken.address] = erc20Balances[makerAddress][zrxToken.address].minus(
+ signedOrder.makerFee,
+ );
+ erc20Balances[takerAddress][defaultMakerAssetAddress] = erc20Balances[takerAddress][
+ defaultMakerAssetAddress
+ ].add(signedOrder.makerAssetAmount);
+ erc20Balances[takerAddress][defaultTakerAssetAddress] = erc20Balances[takerAddress][
+ defaultTakerAssetAddress
+ ].minus(signedOrder.takerAssetAmount);
+ erc20Balances[takerAddress][zrxToken.address] = erc20Balances[takerAddress][zrxToken.address].minus(
+ signedOrder.takerFee,
+ );
+ erc20Balances[feeRecipientAddress][zrxToken.address] = erc20Balances[feeRecipientAddress][
+ zrxToken.address
+ ].add(signedOrder.makerFee.add(signedOrder.takerFee));
+ });
+ await exchangeWrapper.marketBuyOrdersNoThrowAsync(signedOrders, takerAddress, {
+ makerAssetFillAmount,
+ // HACK(albrow): We need to hardcode the gas estimate here because
+ // the Geth gas estimator doesn't work with the way we use
+ // delegatecall and swallow errors.
+ gas: 600000,
+ });
+
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances).to.be.deep.equal(erc20Balances);
+ });
+ });
+
+ describe('batchCancelOrders', () => {
+ it('should be able to cancel multiple signedOrders', async () => {
+ const takerAssetCancelAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount);
+ await exchangeWrapper.batchCancelOrdersAsync(signedOrders, makerAddress);
+
+ await exchangeWrapper.batchFillOrdersNoThrowAsync(signedOrders, takerAddress, {
+ takerAssetFillAmounts: takerAssetCancelAmounts,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(erc20Balances).to.be.deep.equal(newBalances);
+ });
+ });
+
+ describe('getOrdersInfo', () => {
+ beforeEach(async () => {
+ signedOrders = [
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ await orderFactory.newSignedOrderAsync(),
+ ];
+ });
+ it('should get the correct information for multiple unfilled orders', async () => {
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.FILLABLE;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple partially filled orders', async () => {
+ const takerAssetFillAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount.div(2));
+ await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, { takerAssetFillAmounts });
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount.div(2);
+ const expectedOrderStatus = OrderStatus.FILLABLE;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple fully filled orders', async () => {
+ await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress);
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount;
+ const expectedOrderStatus = OrderStatus.FULLY_FILLED;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple cancelled and unfilled orders', async () => {
+ await exchangeWrapper.batchCancelOrdersAsync(signedOrders, makerAddress);
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.CANCELLED;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple cancelled and partially filled orders', async () => {
+ const takerAssetFillAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount.div(2));
+ await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, { takerAssetFillAmounts });
+ await exchangeWrapper.batchCancelOrdersAsync(signedOrders, makerAddress);
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount.div(2);
+ const expectedOrderStatus = OrderStatus.CANCELLED;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple expired and unfilled orders', async () => {
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const timeUntilExpiration = signedOrders[0].expirationTimeSeconds.minus(currentTimestamp).toNumber();
+ await increaseTimeAndMineBlockAsync(timeUntilExpiration);
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = new BigNumber(0);
+ const expectedOrderStatus = OrderStatus.EXPIRED;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for multiple expired and partially filled orders', async () => {
+ const takerAssetFillAmounts = _.map(signedOrders, signedOrder => signedOrder.takerAssetAmount.div(2));
+ await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, { takerAssetFillAmounts });
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const timeUntilExpiration = signedOrders[0].expirationTimeSeconds.minus(currentTimestamp).toNumber();
+ await increaseTimeAndMineBlockAsync(timeUntilExpiration);
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(3);
+ _.forEach(signedOrders, (signedOrder, index) => {
+ const expectedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const expectedTakerAssetFilledAmount = signedOrder.takerAssetAmount.div(2);
+ const expectedOrderStatus = OrderStatus.EXPIRED;
+ const orderInfo = ordersInfo[index];
+ expect(orderInfo.orderHash).to.be.equal(expectedOrderHash);
+ expect(orderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(expectedTakerAssetFilledAmount);
+ expect(orderInfo.orderStatus).to.equal(expectedOrderStatus);
+ });
+ });
+ it('should get the correct information for a mix of unfilled, partially filled, fully filled, cancelled, and expired orders', async () => {
+ const unfilledOrder = await orderFactory.newSignedOrderAsync();
+ const partiallyFilledOrder = await orderFactory.newSignedOrderAsync();
+ await exchangeWrapper.fillOrderAsync(partiallyFilledOrder, takerAddress, {
+ takerAssetFillAmount: partiallyFilledOrder.takerAssetAmount.div(2),
+ });
+ const fullyFilledOrder = await orderFactory.newSignedOrderAsync();
+ await exchangeWrapper.fillOrderAsync(fullyFilledOrder, takerAddress);
+ const cancelledOrder = await orderFactory.newSignedOrderAsync();
+ await exchangeWrapper.cancelOrderAsync(cancelledOrder, makerAddress);
+ const currentTimestamp = await getLatestBlockTimestampAsync();
+ const expiredOrder = await orderFactory.newSignedOrderAsync({
+ expirationTimeSeconds: new BigNumber(currentTimestamp),
+ });
+ signedOrders = [unfilledOrder, partiallyFilledOrder, fullyFilledOrder, cancelledOrder, expiredOrder];
+ const ordersInfo = await exchangeWrapper.getOrdersInfoAsync(signedOrders);
+ expect(ordersInfo.length).to.be.equal(5);
+
+ const expectedUnfilledOrderHash = orderHashUtils.getOrderHashHex(unfilledOrder);
+ const expectedUnfilledTakerAssetFilledAmount = new BigNumber(0);
+ const expectedUnfilledOrderStatus = OrderStatus.FILLABLE;
+ const unfilledOrderInfo = ordersInfo[0];
+ expect(unfilledOrderInfo.orderHash).to.be.equal(expectedUnfilledOrderHash);
+ expect(unfilledOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(
+ expectedUnfilledTakerAssetFilledAmount,
+ );
+ expect(unfilledOrderInfo.orderStatus).to.be.equal(expectedUnfilledOrderStatus);
+
+ const expectedPartialOrderHash = orderHashUtils.getOrderHashHex(partiallyFilledOrder);
+ const expectedPartialTakerAssetFilledAmount = partiallyFilledOrder.takerAssetAmount.div(2);
+ const expectedPartialOrderStatus = OrderStatus.FILLABLE;
+ const partialOrderInfo = ordersInfo[1];
+ expect(partialOrderInfo.orderHash).to.be.equal(expectedPartialOrderHash);
+ expect(partialOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(
+ expectedPartialTakerAssetFilledAmount,
+ );
+ expect(partialOrderInfo.orderStatus).to.be.equal(expectedPartialOrderStatus);
+
+ const expectedFilledOrderHash = orderHashUtils.getOrderHashHex(fullyFilledOrder);
+ const expectedFilledTakerAssetFilledAmount = fullyFilledOrder.takerAssetAmount;
+ const expectedFilledOrderStatus = OrderStatus.FULLY_FILLED;
+ const filledOrderInfo = ordersInfo[2];
+ expect(filledOrderInfo.orderHash).to.be.equal(expectedFilledOrderHash);
+ expect(filledOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(
+ expectedFilledTakerAssetFilledAmount,
+ );
+ expect(filledOrderInfo.orderStatus).to.be.equal(expectedFilledOrderStatus);
+
+ const expectedCancelledOrderHash = orderHashUtils.getOrderHashHex(cancelledOrder);
+ const expectedCancelledTakerAssetFilledAmount = new BigNumber(0);
+ const expectedCancelledOrderStatus = OrderStatus.CANCELLED;
+ const cancelledOrderInfo = ordersInfo[3];
+ expect(cancelledOrderInfo.orderHash).to.be.equal(expectedCancelledOrderHash);
+ expect(cancelledOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(
+ expectedCancelledTakerAssetFilledAmount,
+ );
+ expect(cancelledOrderInfo.orderStatus).to.be.equal(expectedCancelledOrderStatus);
+
+ const expectedExpiredOrderHash = orderHashUtils.getOrderHashHex(expiredOrder);
+ const expectedExpiredTakerAssetFilledAmount = new BigNumber(0);
+ const expectedExpiredOrderStatus = OrderStatus.EXPIRED;
+ const expiredOrderInfo = ordersInfo[4];
+ expect(expiredOrderInfo.orderHash).to.be.equal(expectedExpiredOrderHash);
+ expect(expiredOrderInfo.orderTakerAssetFilledAmount).to.be.bignumber.equal(
+ expectedExpiredTakerAssetFilledAmount,
+ );
+ expect(expiredOrderInfo.orderStatus).to.be.equal(expectedExpiredOrderStatus);
+ });
+ });
+ });
+}); // tslint:disable-line:max-file-line-count
diff --git a/contracts/protocol/test/global_hooks.ts b/contracts/protocol/test/global_hooks.ts
new file mode 100644
index 000000000..f8ace376a
--- /dev/null
+++ b/contracts/protocol/test/global_hooks.ts
@@ -0,0 +1,17 @@
+import { env, EnvVars } from '@0x/dev-utils';
+
+import { coverage, profiler, provider } from '@0x/contracts-test-utils';
+before('start web3 provider', () => {
+ provider.start();
+});
+after('generate coverage report', async () => {
+ if (env.parseBoolean(EnvVars.SolidityCoverage)) {
+ const coverageSubprovider = coverage.getCoverageSubproviderSingleton();
+ await coverageSubprovider.writeCoverageAsync();
+ }
+ if (env.parseBoolean(EnvVars.SolidityProfiler)) {
+ const profilerSubprovider = profiler.getProfilerSubproviderSingleton();
+ await profilerSubprovider.writeProfilerOutputAsync();
+ }
+ provider.stop();
+});
diff --git a/contracts/protocol/test/multisig/asset_proxy_owner.ts b/contracts/protocol/test/multisig/asset_proxy_owner.ts
new file mode 100644
index 000000000..daebfb7fb
--- /dev/null
+++ b/contracts/protocol/test/multisig/asset_proxy_owner.ts
@@ -0,0 +1,506 @@
+import {
+ chaiSetup,
+ constants,
+ expectContractCallFailedAsync,
+ expectContractCreationFailedAsync,
+ expectTransactionFailedAsync,
+ expectTransactionFailedWithoutReasonAsync,
+ increaseTimeAndMineBlockAsync,
+ provider,
+ sendTransactionResult,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { RevertReason } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import { LogWithDecodedArgs } from 'ethereum-types';
+
+import {
+ AssetProxyOwnerAssetProxyRegistrationEventArgs,
+ AssetProxyOwnerContract,
+ AssetProxyOwnerExecutionEventArgs,
+ AssetProxyOwnerExecutionFailureEventArgs,
+ AssetProxyOwnerSubmissionEventArgs,
+} from '../../generated-wrappers/asset_proxy_owner';
+import { MixinAuthorizableContract } from '../../generated-wrappers/mixin_authorizable';
+import { TestAssetProxyOwnerContract } from '../../generated-wrappers/test_asset_proxy_owner';
+import { artifacts } from '../../src/artifacts';
+import { AssetProxyOwnerWrapper } from '../utils/asset_proxy_owner_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+// tslint:disable:no-unnecessary-type-assertion
+describe('AssetProxyOwner', () => {
+ let owners: string[];
+ let authorized: string;
+ let notOwner: string;
+ const REQUIRED_APPROVALS = new BigNumber(2);
+ const SECONDS_TIME_LOCKED = new BigNumber(1000000);
+
+ let erc20Proxy: MixinAuthorizableContract;
+ let erc721Proxy: MixinAuthorizableContract;
+ let testAssetProxyOwner: TestAssetProxyOwnerContract;
+ let assetProxyOwnerWrapper: AssetProxyOwnerWrapper;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ before(async () => {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ owners = [accounts[0], accounts[1]];
+ authorized = accounts[2];
+ notOwner = accounts[3];
+ const initialOwner = accounts[0];
+ erc20Proxy = await MixinAuthorizableContract.deployFrom0xArtifactAsync(
+ artifacts.MixinAuthorizable,
+ provider,
+ txDefaults,
+ );
+ erc721Proxy = await MixinAuthorizableContract.deployFrom0xArtifactAsync(
+ artifacts.MixinAuthorizable,
+ provider,
+ txDefaults,
+ );
+ const defaultAssetProxyContractAddresses: string[] = [];
+ testAssetProxyOwner = await TestAssetProxyOwnerContract.deployFrom0xArtifactAsync(
+ artifacts.TestAssetProxyOwner,
+ provider,
+ txDefaults,
+ owners,
+ defaultAssetProxyContractAddresses,
+ REQUIRED_APPROVALS,
+ SECONDS_TIME_LOCKED,
+ );
+ assetProxyOwnerWrapper = new AssetProxyOwnerWrapper(testAssetProxyOwner, provider);
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.transferOwnership.sendTransactionAsync(testAssetProxyOwner.address, {
+ from: initialOwner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.transferOwnership.sendTransactionAsync(testAssetProxyOwner.address, {
+ from: initialOwner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+
+ describe('constructor', () => {
+ it('should register passed in assetProxyContracts', async () => {
+ const assetProxyContractAddresses = [erc20Proxy.address, erc721Proxy.address];
+ const newMultiSig = await AssetProxyOwnerContract.deployFrom0xArtifactAsync(
+ artifacts.AssetProxyOwner,
+ provider,
+ txDefaults,
+ owners,
+ assetProxyContractAddresses,
+ REQUIRED_APPROVALS,
+ SECONDS_TIME_LOCKED,
+ );
+ const isErc20ProxyRegistered = await newMultiSig.isAssetProxyRegistered.callAsync(erc20Proxy.address);
+ const isErc721ProxyRegistered = await newMultiSig.isAssetProxyRegistered.callAsync(erc721Proxy.address);
+ expect(isErc20ProxyRegistered).to.equal(true);
+ expect(isErc721ProxyRegistered).to.equal(true);
+ });
+ it('should throw if a null address is included in assetProxyContracts', async () => {
+ const assetProxyContractAddresses = [erc20Proxy.address, constants.NULL_ADDRESS];
+ return expectContractCreationFailedAsync(
+ (AssetProxyOwnerContract.deployFrom0xArtifactAsync(
+ artifacts.AssetProxyOwner,
+ provider,
+ txDefaults,
+ owners,
+ assetProxyContractAddresses,
+ REQUIRED_APPROVALS,
+ SECONDS_TIME_LOCKED,
+ ) as any) as sendTransactionResult,
+ RevertReason.InvalidAssetProxy,
+ );
+ });
+ });
+
+ describe('isFunctionRemoveAuthorizedAddressAtIndex', () => {
+ it('should return false if data is not for removeAuthorizedAddressAtIndex', async () => {
+ const notRemoveAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(
+ owners[0],
+ );
+
+ const isFunctionRemoveAuthorizedAddressAtIndex = await testAssetProxyOwner.isFunctionRemoveAuthorizedAddressAtIndex.callAsync(
+ notRemoveAuthorizedAddressData,
+ );
+ expect(isFunctionRemoveAuthorizedAddressAtIndex).to.be.false();
+ });
+
+ it('should return true if data is for removeAuthorizedAddressAtIndex', async () => {
+ const index = new BigNumber(0);
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ owners[0],
+ index,
+ );
+ const isFunctionRemoveAuthorizedAddressAtIndex = await testAssetProxyOwner.isFunctionRemoveAuthorizedAddressAtIndex.callAsync(
+ removeAuthorizedAddressAtIndexData,
+ );
+ expect(isFunctionRemoveAuthorizedAddressAtIndex).to.be.true();
+ });
+ });
+
+ describe('registerAssetProxy', () => {
+ it('should throw if not called by multisig', async () => {
+ const isRegistered = true;
+ return expectTransactionFailedWithoutReasonAsync(
+ testAssetProxyOwner.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, isRegistered, {
+ from: owners[0],
+ }),
+ );
+ });
+
+ it('should register an address if called by multisig after timelock', async () => {
+ const addressToRegister = erc20Proxy.address;
+ const isRegistered = true;
+ const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData(
+ addressToRegister,
+ isRegistered,
+ );
+ const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ testAssetProxyOwner.address,
+ registerAssetProxyData,
+ owners[0],
+ );
+
+ const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+ await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber());
+
+ const executeTxRes = await assetProxyOwnerWrapper.executeTransactionAsync(txId, owners[0]);
+ const registerLog = executeTxRes.logs[0] as LogWithDecodedArgs<
+ AssetProxyOwnerAssetProxyRegistrationEventArgs
+ >;
+ expect(registerLog.args.assetProxyContract).to.equal(addressToRegister);
+ expect(registerLog.args.isRegistered).to.equal(isRegistered);
+
+ const isAssetProxyRegistered = await testAssetProxyOwner.isAssetProxyRegistered.callAsync(
+ addressToRegister,
+ );
+ expect(isAssetProxyRegistered).to.equal(isRegistered);
+ });
+
+ it('should fail if registering a null address', async () => {
+ const addressToRegister = constants.NULL_ADDRESS;
+ const isRegistered = true;
+ const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData(
+ addressToRegister,
+ isRegistered,
+ );
+ const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ testAssetProxyOwner.address,
+ registerAssetProxyData,
+ owners[0],
+ );
+ const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+ await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber());
+
+ const executeTxRes = await assetProxyOwnerWrapper.executeTransactionAsync(txId, owners[0]);
+ const failureLog = executeTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerExecutionFailureEventArgs>;
+ expect(failureLog.args.transactionId).to.be.bignumber.equal(txId);
+
+ const isAssetProxyRegistered = await testAssetProxyOwner.isAssetProxyRegistered.callAsync(
+ addressToRegister,
+ );
+ expect(isAssetProxyRegistered).to.equal(false);
+ });
+ });
+
+ describe('Calling removeAuthorizedAddressAtIndex', () => {
+ const erc20Index = new BigNumber(0);
+ const erc721Index = new BigNumber(1);
+ before('authorize both proxies and register erc20 proxy', async () => {
+ // Only register ERC20 proxy
+ const addressToRegister = erc20Proxy.address;
+ const isRegistered = true;
+ const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData(
+ addressToRegister,
+ isRegistered,
+ );
+ const registerAssetProxySubmitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ testAssetProxyOwner.address,
+ registerAssetProxyData,
+ owners[0],
+ );
+ const registerAssetProxySubmitLog = registerAssetProxySubmitRes.logs[0] as LogWithDecodedArgs<
+ AssetProxyOwnerSubmissionEventArgs
+ >;
+
+ const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(authorized);
+ const erc20AddAuthorizedAddressSubmitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ addAuthorizedAddressData,
+ owners[0],
+ );
+ const erc721AddAuthorizedAddressSubmitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc721Proxy.address,
+ addAuthorizedAddressData,
+ owners[0],
+ );
+ const erc20AddAuthorizedAddressSubmitLog = erc20AddAuthorizedAddressSubmitRes.logs[0] as LogWithDecodedArgs<
+ AssetProxyOwnerSubmissionEventArgs
+ >;
+ const erc721AddAuthorizedAddressSubmitLog = erc721AddAuthorizedAddressSubmitRes
+ .logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+
+ const registerAssetProxyTxId = registerAssetProxySubmitLog.args.transactionId;
+ const erc20AddAuthorizedAddressTxId = erc20AddAuthorizedAddressSubmitLog.args.transactionId;
+ const erc721AddAuthorizedAddressTxId = erc721AddAuthorizedAddressSubmitLog.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(registerAssetProxyTxId, owners[1]);
+ await assetProxyOwnerWrapper.confirmTransactionAsync(erc20AddAuthorizedAddressTxId, owners[1]);
+ await assetProxyOwnerWrapper.confirmTransactionAsync(erc721AddAuthorizedAddressTxId, owners[1]);
+ await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber());
+ await assetProxyOwnerWrapper.executeTransactionAsync(registerAssetProxyTxId, owners[0]);
+ await assetProxyOwnerWrapper.executeTransactionAsync(erc20AddAuthorizedAddressTxId, owners[0], {
+ gas: constants.MAX_EXECUTE_TRANSACTION_GAS,
+ });
+ await assetProxyOwnerWrapper.executeTransactionAsync(erc721AddAuthorizedAddressTxId, owners[0], {
+ gas: constants.MAX_EXECUTE_TRANSACTION_GAS,
+ });
+ });
+
+ describe('validRemoveAuthorizedAddressAtIndexTx', () => {
+ it('should revert if data is not for removeAuthorizedAddressAtIndex and proxy is registered', async () => {
+ const notRemoveAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(
+ authorized,
+ );
+ const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ notRemoveAuthorizedAddressData,
+ owners[0],
+ );
+ const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+ return expectContractCallFailedAsync(
+ testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId),
+ RevertReason.InvalidFunctionSelector,
+ );
+ });
+
+ it('should return true if data is for removeAuthorizedAddressAtIndex and proxy is registered', async () => {
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc20Index,
+ );
+ const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+ const isValidRemoveAuthorizedAddressAtIndexTx = await testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(
+ txId,
+ );
+ expect(isValidRemoveAuthorizedAddressAtIndexTx).to.be.true();
+ });
+
+ it('should revert if data is for removeAuthorizedAddressAtIndex and proxy is not registered', async () => {
+ const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc721Index,
+ );
+ const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc721Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+ return expectContractCallFailedAsync(
+ testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId),
+ RevertReason.UnregisteredAssetProxy,
+ );
+ });
+ });
+
+ describe('executeRemoveAuthorizedAddressAtIndex', () => {
+ it('should throw without the required confirmations', async () => {
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc20Index,
+ );
+ const res = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+
+ return expectTransactionFailedAsync(
+ testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
+ from: owners[1],
+ }),
+ RevertReason.TxNotFullyConfirmed,
+ );
+ });
+
+ it('should throw if tx destination is not registered', async () => {
+ const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc721Index,
+ );
+ const res = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc721Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+
+ return expectTransactionFailedAsync(
+ testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
+ from: owners[1],
+ }),
+ RevertReason.UnregisteredAssetProxy,
+ );
+ });
+
+ it('should throw if tx data is not for removeAuthorizedAddressAtIndex', async () => {
+ const newAuthorized = owners[1];
+ const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(
+ newAuthorized,
+ );
+ const res = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ addAuthorizedAddressData,
+ owners[0],
+ );
+ const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = log.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+
+ return expectTransactionFailedAsync(
+ testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
+ from: owners[1],
+ }),
+ RevertReason.InvalidFunctionSelector,
+ );
+ });
+
+ it('should execute removeAuthorizedAddressAtIndex for registered address if fully confirmed and called by owner', async () => {
+ const isAuthorizedBefore = await erc20Proxy.authorized.callAsync(authorized);
+ expect(isAuthorizedBefore).to.equal(true);
+
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc20Index,
+ );
+ const submitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = submitLog.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+
+ const execRes = await assetProxyOwnerWrapper.executeRemoveAuthorizedAddressAtIndexAsync(
+ txId,
+ owners[0],
+ );
+ const execLog = execRes.logs[1] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>;
+ expect(execLog.args.transactionId).to.be.bignumber.equal(txId);
+
+ const tx = await testAssetProxyOwner.transactions.callAsync(txId);
+ const isExecuted = tx[3];
+ expect(isExecuted).to.equal(true);
+
+ const isAuthorizedAfter = await erc20Proxy.authorized.callAsync(authorized);
+ expect(isAuthorizedAfter).to.equal(false);
+ });
+
+ it('should execute removeAuthorizedAddressAtIndex for registered address if fully confirmed and called by non-owner', async () => {
+ const isAuthorizedBefore = await erc20Proxy.authorized.callAsync(authorized);
+ expect(isAuthorizedBefore).to.equal(true);
+
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc20Index,
+ );
+ const submitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = submitLog.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+
+ const execRes = await assetProxyOwnerWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, notOwner);
+ const execLog = execRes.logs[1] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>;
+ expect(execLog.args.transactionId).to.be.bignumber.equal(txId);
+
+ const tx = await testAssetProxyOwner.transactions.callAsync(txId);
+ const isExecuted = tx[3];
+ expect(isExecuted).to.equal(true);
+
+ const isAuthorizedAfter = await erc20Proxy.authorized.callAsync(authorized);
+ expect(isAuthorizedAfter).to.equal(false);
+ });
+
+ it('should throw if already executed', async () => {
+ const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
+ authorized,
+ erc20Index,
+ );
+ const submitRes = await assetProxyOwnerWrapper.submitTransactionAsync(
+ erc20Proxy.address,
+ removeAuthorizedAddressAtIndexData,
+ owners[0],
+ );
+ const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>;
+ const txId = submitLog.args.transactionId;
+
+ await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]);
+
+ const execRes = await assetProxyOwnerWrapper.executeRemoveAuthorizedAddressAtIndexAsync(
+ txId,
+ owners[0],
+ );
+ const execLog = execRes.logs[1] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>;
+ expect(execLog.args.transactionId).to.be.bignumber.equal(txId);
+
+ const tx = await testAssetProxyOwner.transactions.callAsync(txId);
+ const isExecuted = tx[3];
+ expect(isExecuted).to.equal(true);
+
+ return expectTransactionFailedWithoutReasonAsync(
+ testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
+ from: owners[1],
+ }),
+ );
+ });
+ });
+ });
+});
+// tslint:disable-line max-file-line-count
diff --git a/contracts/protocol/test/utils/asset_proxy_owner_wrapper.ts b/contracts/protocol/test/utils/asset_proxy_owner_wrapper.ts
new file mode 100644
index 000000000..df23658d8
--- /dev/null
+++ b/contracts/protocol/test/utils/asset_proxy_owner_wrapper.ts
@@ -0,0 +1,70 @@
+import { LogDecoder } from '@0x/contracts-test-utils';
+import { artifacts as tokensArtifacts } from '@0x/contracts-tokens';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { AssetProxyOwnerContract } from '../../generated-wrappers/asset_proxy_owner';
+import { artifacts } from '../../src/artifacts';
+
+export class AssetProxyOwnerWrapper {
+ private readonly _assetProxyOwner: AssetProxyOwnerContract;
+ private readonly _web3Wrapper: Web3Wrapper;
+ private readonly _logDecoder: LogDecoder;
+ constructor(assetproxyOwnerContract: AssetProxyOwnerContract, provider: Provider) {
+ this._assetProxyOwner = assetproxyOwnerContract;
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._logDecoder = new LogDecoder(this._web3Wrapper, { ...artifacts, ...tokensArtifacts });
+ }
+ public async submitTransactionAsync(
+ destination: string,
+ data: string,
+ from: string,
+ opts: { value?: BigNumber } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const value = _.isUndefined(opts.value) ? new BigNumber(0) : opts.value;
+ const txHash = await this._assetProxyOwner.submitTransaction.sendTransactionAsync(destination, value, data, {
+ from,
+ });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async confirmTransactionAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._assetProxyOwner.confirmTransaction.sendTransactionAsync(txId, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async revokeConfirmationAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._assetProxyOwner.revokeConfirmation.sendTransactionAsync(txId, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async executeTransactionAsync(
+ txId: BigNumber,
+ from: string,
+ opts: { gas?: number } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._assetProxyOwner.executeTransaction.sendTransactionAsync(txId, {
+ from,
+ gas: opts.gas,
+ });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async executeRemoveAuthorizedAddressAtIndexAsync(
+ txId: BigNumber,
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ // tslint:disable-next-line:no-unnecessary-type-assertion
+ const txHash = await (this
+ ._assetProxyOwner as AssetProxyOwnerContract).executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(
+ txId,
+ {
+ from,
+ },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+}
diff --git a/contracts/protocol/test/utils/asset_wrapper.ts b/contracts/protocol/test/utils/asset_wrapper.ts
new file mode 100644
index 000000000..e4090ad74
--- /dev/null
+++ b/contracts/protocol/test/utils/asset_wrapper.ts
@@ -0,0 +1,222 @@
+import { AbstractAssetWrapper, constants } from '@0x/contracts-test-utils';
+import { assetDataUtils } from '@0x/order-utils';
+import { AssetProxyId } from '@0x/types';
+import { BigNumber, errorUtils } from '@0x/utils';
+import * as _ from 'lodash';
+
+import { ERC20Wrapper } from './erc20_wrapper';
+import { ERC721Wrapper } from './erc721_wrapper';
+
+interface ProxyIdToAssetWrappers {
+ [proxyId: string]: AbstractAssetWrapper;
+}
+
+/**
+ * This class abstracts away the differences between ERC20 and ERC721 tokens so that
+ * the logic that uses it does not need to care what standard a token belongs to.
+ */
+export class AssetWrapper {
+ private readonly _proxyIdToAssetWrappers: ProxyIdToAssetWrappers;
+ constructor(assetWrappers: AbstractAssetWrapper[]) {
+ this._proxyIdToAssetWrappers = {};
+ _.each(assetWrappers, assetWrapper => {
+ const proxyId = assetWrapper.getProxyId();
+ this._proxyIdToAssetWrappers[proxyId] = assetWrapper;
+ });
+ }
+ public async getBalanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
+ const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
+ switch (proxyId) {
+ case AssetProxyId.ERC20: {
+ const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
+ const balance = await erc20Wrapper.getBalanceAsync(userAddress, assetData);
+ return balance;
+ }
+ case AssetProxyId.ERC721: {
+ const assetWrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
+ const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData);
+ const isOwner = await assetWrapper.isOwnerAsync(
+ userAddress,
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ const balance = isOwner ? new BigNumber(1) : new BigNumber(0);
+ return balance;
+ }
+ default:
+ throw errorUtils.spawnSwitchErr('proxyId', proxyId);
+ }
+ }
+ public async setBalanceAsync(userAddress: string, assetData: string, desiredBalance: BigNumber): Promise<void> {
+ const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
+ switch (proxyId) {
+ case AssetProxyId.ERC20: {
+ const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
+ await erc20Wrapper.setBalanceAsync(userAddress, assetData, desiredBalance);
+ return;
+ }
+ case AssetProxyId.ERC721: {
+ if (!desiredBalance.eq(0) && !desiredBalance.eq(1)) {
+ throw new Error(`Balance for ERC721 token can only be set to 0 or 1. Got: ${desiredBalance}`);
+ }
+ const erc721Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
+ const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData);
+ const doesTokenExist = erc721Wrapper.doesTokenExistAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ if (!doesTokenExist && desiredBalance.eq(1)) {
+ await erc721Wrapper.mintAsync(assetProxyData.tokenAddress, assetProxyData.tokenId, userAddress);
+ return;
+ } else if (!doesTokenExist && desiredBalance.eq(0)) {
+ return; // noop
+ }
+ const tokenOwner = await erc721Wrapper.ownerOfAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ if (userAddress !== tokenOwner && desiredBalance.eq(1)) {
+ await erc721Wrapper.transferFromAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ tokenOwner,
+ userAddress,
+ );
+ } else if (tokenOwner === userAddress && desiredBalance.eq(0)) {
+ // Transfer token to someone else
+ const userAddresses = await (erc721Wrapper as any)._web3Wrapper.getAvailableAddressesAsync();
+ const nonOwner = _.find(userAddresses, a => a !== userAddress);
+ await erc721Wrapper.transferFromAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ tokenOwner,
+ nonOwner,
+ );
+ return;
+ } else if (
+ (userAddress !== tokenOwner && desiredBalance.eq(0)) ||
+ (tokenOwner === userAddress && desiredBalance.eq(1))
+ ) {
+ return; // noop
+ }
+ break;
+ }
+ default:
+ throw errorUtils.spawnSwitchErr('proxyId', proxyId);
+ }
+ }
+ public async getProxyAllowanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
+ const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
+ switch (proxyId) {
+ case AssetProxyId.ERC20: {
+ const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
+ const allowance = await erc20Wrapper.getProxyAllowanceAsync(userAddress, assetData);
+ return allowance;
+ }
+ case AssetProxyId.ERC721: {
+ const assetWrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
+ const erc721ProxyData = assetDataUtils.decodeERC721AssetData(assetData);
+ const isProxyApprovedForAll = await assetWrapper.isProxyApprovedForAllAsync(
+ userAddress,
+ erc721ProxyData.tokenAddress,
+ );
+ if (isProxyApprovedForAll) {
+ return constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
+ }
+
+ const isProxyApproved = await assetWrapper.isProxyApprovedAsync(
+ erc721ProxyData.tokenAddress,
+ erc721ProxyData.tokenId,
+ );
+ const allowance = isProxyApproved ? new BigNumber(1) : new BigNumber(0);
+ return allowance;
+ }
+ default:
+ throw errorUtils.spawnSwitchErr('proxyId', proxyId);
+ }
+ }
+ public async setProxyAllowanceAsync(
+ userAddress: string,
+ assetData: string,
+ desiredAllowance: BigNumber,
+ ): Promise<void> {
+ const proxyId = assetDataUtils.decodeAssetProxyId(assetData);
+ switch (proxyId) {
+ case AssetProxyId.ERC20: {
+ const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper;
+ await erc20Wrapper.setAllowanceAsync(userAddress, assetData, desiredAllowance);
+ return;
+ }
+ case AssetProxyId.ERC721: {
+ if (
+ !desiredAllowance.eq(0) &&
+ !desiredAllowance.eq(1) &&
+ !desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)
+ ) {
+ throw new Error(
+ `Allowance for ERC721 token can only be set to 0, 1 or 2^256-1. Got: ${desiredAllowance}`,
+ );
+ }
+ const erc721Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper;
+ const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData);
+
+ const doesTokenExist = await erc721Wrapper.doesTokenExistAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ if (!doesTokenExist) {
+ throw new Error(
+ `Cannot setProxyAllowance on non-existent token: ${assetProxyData.tokenAddress} ${
+ assetProxyData.tokenId
+ }`,
+ );
+ }
+ const isProxyApprovedForAll = await erc721Wrapper.isProxyApprovedForAllAsync(
+ userAddress,
+ assetProxyData.tokenAddress,
+ );
+ if (!isProxyApprovedForAll && desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
+ const isApproved = true;
+ await erc721Wrapper.approveProxyForAllAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ isApproved,
+ );
+ } else if (isProxyApprovedForAll && desiredAllowance.eq(0)) {
+ const isApproved = false;
+ await erc721Wrapper.approveProxyForAllAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ isApproved,
+ );
+ } else if (isProxyApprovedForAll && desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
+ return; // Noop
+ }
+
+ const isProxyApproved = await erc721Wrapper.isProxyApprovedAsync(
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ if (!isProxyApproved && desiredAllowance.eq(1)) {
+ await erc721Wrapper.approveProxyAsync(assetProxyData.tokenAddress, assetProxyData.tokenId);
+ } else if (isProxyApproved && desiredAllowance.eq(0)) {
+ // Remove approval
+ await erc721Wrapper.approveAsync(
+ constants.NULL_ADDRESS,
+ assetProxyData.tokenAddress,
+ assetProxyData.tokenId,
+ );
+ } else if (
+ (!isProxyApproved && desiredAllowance.eq(0)) ||
+ (isProxyApproved && desiredAllowance.eq(1))
+ ) {
+ return; // noop
+ }
+
+ break;
+ }
+ default:
+ throw errorUtils.spawnSwitchErr('proxyId', proxyId);
+ }
+ }
+}
diff --git a/contracts/protocol/test/utils/erc20_wrapper.ts b/contracts/protocol/test/utils/erc20_wrapper.ts
new file mode 100644
index 000000000..e5ab0e36c
--- /dev/null
+++ b/contracts/protocol/test/utils/erc20_wrapper.ts
@@ -0,0 +1,179 @@
+import { constants, ERC20BalancesByOwner, txDefaults } from '@0x/contracts-test-utils';
+import { artifacts as tokensArtifacts, DummyERC20TokenContract } from '@0x/contracts-tokens';
+import { assetDataUtils } from '@0x/order-utils';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy';
+import { artifacts } from '../../src/artifacts';
+
+export class ERC20Wrapper {
+ private readonly _tokenOwnerAddresses: string[];
+ private readonly _contractOwnerAddress: string;
+ private readonly _web3Wrapper: Web3Wrapper;
+ private readonly _provider: Provider;
+ private readonly _dummyTokenContracts: DummyERC20TokenContract[];
+ private _proxyContract?: ERC20ProxyContract;
+ private _proxyIdIfExists?: string;
+ /**
+ * Instanitates an ERC20Wrapper
+ * @param provider Web3 provider to use for all JSON RPC requests
+ * @param tokenOwnerAddresses Addresses that we want to endow as owners for dummy ERC20 tokens
+ * @param contractOwnerAddress Desired owner of the contract
+ * Instance of ERC20Wrapper
+ */
+ constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) {
+ this._dummyTokenContracts = [];
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._provider = provider;
+ this._tokenOwnerAddresses = tokenOwnerAddresses;
+ this._contractOwnerAddress = contractOwnerAddress;
+ }
+ public async deployDummyTokensAsync(
+ numberToDeploy: number,
+ decimals: BigNumber,
+ ): Promise<DummyERC20TokenContract[]> {
+ for (let i = 0; i < numberToDeploy; i++) {
+ this._dummyTokenContracts.push(
+ await DummyERC20TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyERC20Token,
+ this._provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ decimals,
+ constants.DUMMY_TOKEN_TOTAL_SUPPLY,
+ ),
+ );
+ }
+ return this._dummyTokenContracts;
+ }
+ public async deployProxyAsync(): Promise<ERC20ProxyContract> {
+ this._proxyContract = await ERC20ProxyContract.deployFrom0xArtifactAsync(
+ artifacts.ERC20Proxy,
+ this._provider,
+ txDefaults,
+ );
+ this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync();
+ return this._proxyContract;
+ }
+ public getProxyId(): string {
+ this._validateProxyContractExistsOrThrow();
+ return this._proxyIdIfExists as string;
+ }
+ public async setBalancesAndAllowancesAsync(): Promise<void> {
+ this._validateDummyTokenContractsExistOrThrow();
+ this._validateProxyContractExistsOrThrow();
+ for (const dummyTokenContract of this._dummyTokenContracts) {
+ for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await dummyTokenContract.setBalance.sendTransactionAsync(
+ tokenOwnerAddress,
+ constants.INITIAL_ERC20_BALANCE,
+ { from: this._contractOwnerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await dummyTokenContract.approve.sendTransactionAsync(
+ (this._proxyContract as ERC20ProxyContract).address,
+ constants.INITIAL_ERC20_ALLOWANCE,
+ { from: tokenOwnerAddress },
+ ),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ }
+ }
+ public async getBalanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
+ const tokenContract = this._getTokenContractFromAssetData(assetData);
+ const balance = new BigNumber(await tokenContract.balanceOf.callAsync(userAddress));
+ return balance;
+ }
+ public async setBalanceAsync(userAddress: string, assetData: string, amount: BigNumber): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(assetData);
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.setBalance.sendTransactionAsync(userAddress, amount, {
+ from: this._contractOwnerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async getProxyAllowanceAsync(userAddress: string, assetData: string): Promise<BigNumber> {
+ const tokenContract = this._getTokenContractFromAssetData(assetData);
+ const proxyAddress = (this._proxyContract as ERC20ProxyContract).address;
+ const allowance = new BigNumber(await tokenContract.allowance.callAsync(userAddress, proxyAddress));
+ return allowance;
+ }
+ public async setAllowanceAsync(userAddress: string, assetData: string, amount: BigNumber): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(assetData);
+ const proxyAddress = (this._proxyContract as ERC20ProxyContract).address;
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.approve.sendTransactionAsync(proxyAddress, amount, {
+ from: userAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async getBalancesAsync(): Promise<ERC20BalancesByOwner> {
+ this._validateDummyTokenContractsExistOrThrow();
+ const balancesByOwner: ERC20BalancesByOwner = {};
+ const balances: BigNumber[] = [];
+ const balanceInfo: Array<{ tokenOwnerAddress: string; tokenAddress: string }> = [];
+ for (const dummyTokenContract of this._dummyTokenContracts) {
+ for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
+ balances.push(await dummyTokenContract.balanceOf.callAsync(tokenOwnerAddress));
+ balanceInfo.push({
+ tokenOwnerAddress,
+ tokenAddress: dummyTokenContract.address,
+ });
+ }
+ }
+ _.forEach(balances, (balance, balanceIndex) => {
+ const tokenAddress = balanceInfo[balanceIndex].tokenAddress;
+ const tokenOwnerAddress = balanceInfo[balanceIndex].tokenOwnerAddress;
+ if (_.isUndefined(balancesByOwner[tokenOwnerAddress])) {
+ balancesByOwner[tokenOwnerAddress] = {};
+ }
+ const wrappedBalance = new BigNumber(balance);
+ balancesByOwner[tokenOwnerAddress][tokenAddress] = wrappedBalance;
+ });
+ return balancesByOwner;
+ }
+ public addDummyTokenContract(dummy: DummyERC20TokenContract): void {
+ if (!_.isUndefined(this._dummyTokenContracts)) {
+ this._dummyTokenContracts.push(dummy);
+ }
+ }
+ public addTokenOwnerAddress(address: string): void {
+ this._tokenOwnerAddresses.push(address);
+ }
+ public getTokenOwnerAddresses(): string[] {
+ return this._tokenOwnerAddresses;
+ }
+ public getTokenAddresses(): string[] {
+ const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address);
+ return tokenAddresses;
+ }
+ private _getTokenContractFromAssetData(assetData: string): DummyERC20TokenContract {
+ const erc20ProxyData = assetDataUtils.decodeERC20AssetData(assetData);
+ const tokenAddress = erc20ProxyData.tokenAddress;
+ const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress);
+ if (_.isUndefined(tokenContractIfExists)) {
+ throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`);
+ }
+ return tokenContractIfExists;
+ }
+ private _validateDummyTokenContractsExistOrThrow(): void {
+ if (_.isUndefined(this._dummyTokenContracts)) {
+ throw new Error('Dummy ERC20 tokens not yet deployed, please call "deployDummyTokensAsync"');
+ }
+ }
+ private _validateProxyContractExistsOrThrow(): void {
+ if (_.isUndefined(this._proxyContract)) {
+ throw new Error('ERC20 proxy contract not yet deployed, please call "deployProxyAsync"');
+ }
+ }
+}
diff --git a/contracts/protocol/test/utils/erc721_wrapper.ts b/contracts/protocol/test/utils/erc721_wrapper.ts
new file mode 100644
index 000000000..1f658ce86
--- /dev/null
+++ b/contracts/protocol/test/utils/erc721_wrapper.ts
@@ -0,0 +1,236 @@
+import { constants, ERC721TokenIdsByOwner, txDefaults } from '@0x/contracts-test-utils';
+import { artifacts as tokensArtifacts, DummyERC721TokenContract } from '@0x/contracts-tokens';
+import { generatePseudoRandomSalt } from '@0x/order-utils';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy';
+import { artifacts } from '../../src/artifacts';
+
+export class ERC721Wrapper {
+ private readonly _tokenOwnerAddresses: string[];
+ private readonly _contractOwnerAddress: string;
+ private readonly _web3Wrapper: Web3Wrapper;
+ private readonly _provider: Provider;
+ private readonly _dummyTokenContracts: DummyERC721TokenContract[];
+ private _proxyContract?: ERC721ProxyContract;
+ private _proxyIdIfExists?: string;
+ private _initialTokenIdsByOwner: ERC721TokenIdsByOwner = {};
+ constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) {
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._provider = provider;
+ this._dummyTokenContracts = [];
+ this._tokenOwnerAddresses = tokenOwnerAddresses;
+ this._contractOwnerAddress = contractOwnerAddress;
+ }
+ public async deployDummyTokensAsync(): Promise<DummyERC721TokenContract[]> {
+ // tslint:disable-next-line:no-unused-variable
+ for (const i of _.times(constants.NUM_DUMMY_ERC721_TO_DEPLOY)) {
+ this._dummyTokenContracts.push(
+ await DummyERC721TokenContract.deployFrom0xArtifactAsync(
+ tokensArtifacts.DummyERC721Token,
+ this._provider,
+ txDefaults,
+ constants.DUMMY_TOKEN_NAME,
+ constants.DUMMY_TOKEN_SYMBOL,
+ ),
+ );
+ }
+ return this._dummyTokenContracts;
+ }
+ public async deployProxyAsync(): Promise<ERC721ProxyContract> {
+ this._proxyContract = await ERC721ProxyContract.deployFrom0xArtifactAsync(
+ artifacts.ERC721Proxy,
+ this._provider,
+ txDefaults,
+ );
+ this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync();
+ return this._proxyContract;
+ }
+ public getProxyId(): string {
+ this._validateProxyContractExistsOrThrow();
+ return this._proxyIdIfExists as string;
+ }
+ public async setBalancesAndAllowancesAsync(): Promise<void> {
+ this._validateDummyTokenContractsExistOrThrow();
+ this._validateProxyContractExistsOrThrow();
+ this._initialTokenIdsByOwner = {};
+ for (const dummyTokenContract of this._dummyTokenContracts) {
+ for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
+ // tslint:disable-next-line:no-unused-variable
+ for (const i of _.times(constants.NUM_ERC721_TOKENS_TO_MINT)) {
+ const tokenId = generatePseudoRandomSalt();
+ await this.mintAsync(dummyTokenContract.address, tokenId, tokenOwnerAddress);
+ if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress])) {
+ this._initialTokenIdsByOwner[tokenOwnerAddress] = {
+ [dummyTokenContract.address]: [],
+ };
+ }
+ if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address])) {
+ this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address] = [];
+ }
+ this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address].push(tokenId);
+
+ await this.approveProxyAsync(dummyTokenContract.address, tokenId);
+ }
+ }
+ }
+ }
+ public async doesTokenExistAsync(tokenAddress: string, tokenId: BigNumber): Promise<boolean> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const owner = await tokenContract.ownerOf.callAsync(tokenId);
+ const doesExist = owner !== constants.NULL_ADDRESS;
+ return doesExist;
+ }
+ public async approveProxyAsync(tokenAddress: string, tokenId: BigNumber): Promise<void> {
+ const proxyAddress = (this._proxyContract as ERC721ProxyContract).address;
+ await this.approveAsync(proxyAddress, tokenAddress, tokenId);
+ }
+ public async approveProxyForAllAsync(tokenAddress: string, tokenId: BigNumber, isApproved: boolean): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const tokenOwner = await this.ownerOfAsync(tokenAddress, tokenId);
+ const proxyAddress = (this._proxyContract as ERC721ProxyContract).address;
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.setApprovalForAll.sendTransactionAsync(proxyAddress, isApproved, {
+ from: tokenOwner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async approveAsync(to: string, tokenAddress: string, tokenId: BigNumber): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const tokenOwner = await this.ownerOfAsync(tokenAddress, tokenId);
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.approve.sendTransactionAsync(to, tokenId, {
+ from: tokenOwner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async transferFromAsync(
+ tokenAddress: string,
+ tokenId: BigNumber,
+ currentOwner: string,
+ userAddress: string,
+ ): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.transferFrom.sendTransactionAsync(currentOwner, userAddress, tokenId, {
+ from: currentOwner,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async mintAsync(tokenAddress: string, tokenId: BigNumber, userAddress: string): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.mint.sendTransactionAsync(userAddress, tokenId, {
+ from: this._contractOwnerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async burnAsync(tokenAddress: string, tokenId: BigNumber, owner: string): Promise<void> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ await this._web3Wrapper.awaitTransactionSuccessAsync(
+ await tokenContract.burn.sendTransactionAsync(owner, tokenId, {
+ from: this._contractOwnerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ }
+ public async ownerOfAsync(tokenAddress: string, tokenId: BigNumber): Promise<string> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const owner = await tokenContract.ownerOf.callAsync(tokenId);
+ return owner;
+ }
+ public async isOwnerAsync(userAddress: string, tokenAddress: string, tokenId: BigNumber): Promise<boolean> {
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const tokenOwner = await tokenContract.ownerOf.callAsync(tokenId);
+ const isOwner = tokenOwner === userAddress;
+ return isOwner;
+ }
+ public async isProxyApprovedForAllAsync(userAddress: string, tokenAddress: string): Promise<boolean> {
+ this._validateProxyContractExistsOrThrow();
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const operator = (this._proxyContract as ERC721ProxyContract).address;
+ const didApproveAll = await tokenContract.isApprovedForAll.callAsync(userAddress, operator);
+ return didApproveAll;
+ }
+ public async isProxyApprovedAsync(tokenAddress: string, tokenId: BigNumber): Promise<boolean> {
+ this._validateProxyContractExistsOrThrow();
+ const tokenContract = this._getTokenContractFromAssetData(tokenAddress);
+ const approvedAddress = await tokenContract.getApproved.callAsync(tokenId);
+ const proxyAddress = (this._proxyContract as ERC721ProxyContract).address;
+ const isProxyAnApprovedOperator = approvedAddress === proxyAddress;
+ return isProxyAnApprovedOperator;
+ }
+ public async getBalancesAsync(): Promise<ERC721TokenIdsByOwner> {
+ this._validateDummyTokenContractsExistOrThrow();
+ this._validateBalancesAndAllowancesSetOrThrow();
+ const tokenIdsByOwner: ERC721TokenIdsByOwner = {};
+ const tokenOwnerAddresses: string[] = [];
+ const tokenInfo: Array<{ tokenId: BigNumber; tokenAddress: string }> = [];
+ for (const dummyTokenContract of this._dummyTokenContracts) {
+ for (const tokenOwnerAddress of this._tokenOwnerAddresses) {
+ const initialTokenOwnerIds = this._initialTokenIdsByOwner[tokenOwnerAddress][
+ dummyTokenContract.address
+ ];
+ for (const tokenId of initialTokenOwnerIds) {
+ tokenOwnerAddresses.push(await dummyTokenContract.ownerOf.callAsync(tokenId));
+ tokenInfo.push({
+ tokenId,
+ tokenAddress: dummyTokenContract.address,
+ });
+ }
+ }
+ }
+ _.forEach(tokenOwnerAddresses, (tokenOwnerAddress, ownerIndex) => {
+ const tokenAddress = tokenInfo[ownerIndex].tokenAddress;
+ const tokenId = tokenInfo[ownerIndex].tokenId;
+ if (_.isUndefined(tokenIdsByOwner[tokenOwnerAddress])) {
+ tokenIdsByOwner[tokenOwnerAddress] = {
+ [tokenAddress]: [],
+ };
+ }
+ if (_.isUndefined(tokenIdsByOwner[tokenOwnerAddress][tokenAddress])) {
+ tokenIdsByOwner[tokenOwnerAddress][tokenAddress] = [];
+ }
+ tokenIdsByOwner[tokenOwnerAddress][tokenAddress].push(tokenId);
+ });
+ return tokenIdsByOwner;
+ }
+ public getTokenOwnerAddresses(): string[] {
+ return this._tokenOwnerAddresses;
+ }
+ public getTokenAddresses(): string[] {
+ const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address);
+ return tokenAddresses;
+ }
+ private _getTokenContractFromAssetData(tokenAddress: string): DummyERC721TokenContract {
+ const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress);
+ if (_.isUndefined(tokenContractIfExists)) {
+ throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`);
+ }
+ return tokenContractIfExists;
+ }
+ private _validateDummyTokenContractsExistOrThrow(): void {
+ if (_.isUndefined(this._dummyTokenContracts)) {
+ throw new Error('Dummy ERC721 tokens not yet deployed, please call "deployDummyTokensAsync"');
+ }
+ }
+ private _validateProxyContractExistsOrThrow(): void {
+ if (_.isUndefined(this._proxyContract)) {
+ throw new Error('ERC721 proxy contract not yet deployed, please call "deployProxyAsync"');
+ }
+ }
+ private _validateBalancesAndAllowancesSetOrThrow(): void {
+ if (_.keys(this._initialTokenIdsByOwner).length === 0) {
+ throw new Error(
+ 'Dummy ERC721 balances and allowances not yet set, please call "setBalancesAndAllowancesAsync"',
+ );
+ }
+ }
+}
diff --git a/contracts/protocol/test/utils/exchange_wrapper.ts b/contracts/protocol/test/utils/exchange_wrapper.ts
new file mode 100644
index 000000000..cb6dce901
--- /dev/null
+++ b/contracts/protocol/test/utils/exchange_wrapper.ts
@@ -0,0 +1,281 @@
+import {
+ FillResults,
+ formatters,
+ LogDecoder,
+ OrderInfo,
+ orderUtils,
+ SignedTransaction,
+} from '@0x/contracts-test-utils';
+import { artifacts as tokensArtifacts } from '@0x/contracts-tokens';
+import { SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
+
+import { ExchangeContract } from '../../generated-wrappers/exchange';
+import { artifacts } from '../../src/artifacts';
+
+export class ExchangeWrapper {
+ private readonly _exchange: ExchangeContract;
+ private readonly _web3Wrapper: Web3Wrapper;
+ private readonly _logDecoder: LogDecoder;
+ constructor(exchangeContract: ExchangeContract, provider: Provider) {
+ this._exchange = exchangeContract;
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._logDecoder = new LogDecoder(this._web3Wrapper, { ...artifacts, ...tokensArtifacts });
+ }
+ public async fillOrderAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const txHash = await this._exchange.fillOrder.sendTransactionAsync(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ { from },
+ );
+ const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return txReceipt;
+ }
+ public async cancelOrderAsync(signedOrder: SignedOrder, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createCancel(signedOrder);
+ const txHash = await this._exchange.cancelOrder.sendTransactionAsync(params.order, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async fillOrKillOrderAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const txHash = await this._exchange.fillOrKillOrder.sendTransactionAsync(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async fillOrderNoThrowAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber; gas?: number } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const txHash = await this._exchange.fillOrderNoThrow.sendTransactionAsync(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ { from, gas: opts.gas },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async batchFillOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmounts?: BigNumber[] } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
+ const txHash = await this._exchange.batchFillOrders.sendTransactionAsync(
+ params.orders,
+ params.takerAssetFillAmounts,
+ params.signatures,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async batchFillOrKillOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmounts?: BigNumber[] } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
+ const txHash = await this._exchange.batchFillOrKillOrders.sendTransactionAsync(
+ params.orders,
+ params.takerAssetFillAmounts,
+ params.signatures,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async batchFillOrdersNoThrowAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmounts?: BigNumber[]; gas?: number } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts);
+ const txHash = await this._exchange.batchFillOrdersNoThrow.sendTransactionAsync(
+ params.orders,
+ params.takerAssetFillAmounts,
+ params.signatures,
+ { from, gas: opts.gas },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async marketSellOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmount: BigNumber },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount);
+ const txHash = await this._exchange.marketSellOrders.sendTransactionAsync(
+ params.orders,
+ params.takerAssetFillAmount,
+ params.signatures,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async marketSellOrdersNoThrowAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { takerAssetFillAmount: BigNumber; gas?: number },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount);
+ const txHash = await this._exchange.marketSellOrdersNoThrow.sendTransactionAsync(
+ params.orders,
+ params.takerAssetFillAmount,
+ params.signatures,
+ { from, gas: opts.gas },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async marketBuyOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { makerAssetFillAmount: BigNumber },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount);
+ const txHash = await this._exchange.marketBuyOrders.sendTransactionAsync(
+ params.orders,
+ params.makerAssetFillAmount,
+ params.signatures,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async marketBuyOrdersNoThrowAsync(
+ orders: SignedOrder[],
+ from: string,
+ opts: { makerAssetFillAmount: BigNumber; gas?: number },
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount);
+ const txHash = await this._exchange.marketBuyOrdersNoThrow.sendTransactionAsync(
+ params.orders,
+ params.makerAssetFillAmount,
+ params.signatures,
+ { from, gas: opts.gas },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async batchCancelOrdersAsync(
+ orders: SignedOrder[],
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = formatters.createBatchCancel(orders);
+ const txHash = await this._exchange.batchCancelOrders.sendTransactionAsync(params.orders, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async cancelOrdersUpToAsync(salt: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._exchange.cancelOrdersUpTo.sendTransactionAsync(salt, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async registerAssetProxyAsync(
+ assetProxyAddress: string,
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._exchange.registerAssetProxy.sendTransactionAsync(assetProxyAddress, { from });
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async executeTransactionAsync(
+ signedTx: SignedTransaction,
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const txHash = await this._exchange.executeTransaction.sendTransactionAsync(
+ signedTx.salt,
+ signedTx.signerAddress,
+ signedTx.data,
+ signedTx.signature,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async getTakerAssetFilledAmountAsync(orderHashHex: string): Promise<BigNumber> {
+ const filledAmount = await this._exchange.filled.callAsync(orderHashHex);
+ return filledAmount;
+ }
+ public async isCancelledAsync(orderHashHex: string): Promise<boolean> {
+ const isCancelled = await this._exchange.cancelled.callAsync(orderHashHex);
+ return isCancelled;
+ }
+ public async getOrderEpochAsync(makerAddress: string, senderAddress: string): Promise<BigNumber> {
+ const orderEpoch = await this._exchange.orderEpoch.callAsync(makerAddress, senderAddress);
+ return orderEpoch;
+ }
+ public async getOrderInfoAsync(signedOrder: SignedOrder): Promise<OrderInfo> {
+ const orderInfo = await this._exchange.getOrderInfo.callAsync(signedOrder);
+ return orderInfo;
+ }
+ public async getOrdersInfoAsync(signedOrders: SignedOrder[]): Promise<OrderInfo[]> {
+ const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[];
+ return ordersInfo;
+ }
+ public async matchOrdersAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ from: string,
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight);
+ const txHash = await this._exchange.matchOrders.sendTransactionAsync(
+ params.left,
+ params.right,
+ params.leftSignature,
+ params.rightSignature,
+ { from },
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async getFillOrderResultsAsync(
+ signedOrder: SignedOrder,
+ from: string,
+ opts: { takerAssetFillAmount?: BigNumber } = {},
+ ): Promise<FillResults> {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const fillResults = await this._exchange.fillOrder.callAsync(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ { from },
+ );
+ return fillResults;
+ }
+ public abiEncodeFillOrder(signedOrder: SignedOrder, opts: { takerAssetFillAmount?: BigNumber } = {}): string {
+ const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
+ const data = this._exchange.fillOrder.getABIEncodedTransactionData(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ );
+ return data;
+ }
+ public getExchangeAddress(): string {
+ return this._exchange.address;
+ }
+}
diff --git a/contracts/protocol/test/utils/fill_order_combinatorial_utils.ts b/contracts/protocol/test/utils/fill_order_combinatorial_utils.ts
new file mode 100644
index 000000000..5d0ea07a8
--- /dev/null
+++ b/contracts/protocol/test/utils/fill_order_combinatorial_utils.ts
@@ -0,0 +1,928 @@
+import { artifacts as libsArtifacts, TestLibsContract } from '@0x/contracts-libs';
+import {
+ AllowanceAmountScenario,
+ AssetDataScenario,
+ BalanceAmountScenario,
+ chaiSetup,
+ constants,
+ expectTransactionFailedAsync,
+ ExpirationTimeSecondsScenario,
+ FeeRecipientAddressScenario,
+ FillScenario,
+ OrderAssetAmountScenario,
+ orderUtils,
+ signingUtils,
+ TakerAssetFillAmountScenario,
+ TakerScenario,
+ TraderStateScenario,
+} from '@0x/contracts-test-utils';
+import {
+ assetDataUtils,
+ BalanceAndProxyAllowanceLazyStore,
+ ExchangeTransferSimulator,
+ orderHashUtils,
+ OrderStateUtils,
+ OrderValidationUtils,
+} from '@0x/order-utils';
+import { AssetProxyId, RevertReason, SignatureType, SignedOrder } from '@0x/types';
+import { BigNumber, errorUtils, logUtils } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as chai from 'chai';
+import { LogWithDecodedArgs, Provider, TxData } from 'ethereum-types';
+import * as _ from 'lodash';
+import 'make-promises-safe';
+
+import { ExchangeContract, ExchangeFillEventArgs } from '../../generated-wrappers/exchange';
+import { artifacts } from '../../src/artifacts';
+
+import { AssetWrapper } from './asset_wrapper';
+import { ERC20Wrapper } from './erc20_wrapper';
+import { ERC721Wrapper } from './erc721_wrapper';
+import { ExchangeWrapper } from './exchange_wrapper';
+import { OrderFactoryFromScenario } from './order_factory_from_scenario';
+import { SimpleAssetBalanceAndProxyAllowanceFetcher } from './simple_asset_balance_and_proxy_allowance_fetcher';
+import { SimpleOrderFilledCancelledFetcher } from './simple_order_filled_cancelled_fetcher';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+/**
+ * Instantiates a new instance of FillOrderCombinatorialUtils. Since this method has some
+ * required async setup, a factory method is required.
+ * @param web3Wrapper Web3Wrapper instance
+ * @param txDefaults Default Ethereum tx options
+ * @return FillOrderCombinatorialUtils instance
+ */
+export async function fillOrderCombinatorialUtilsFactoryAsync(
+ web3Wrapper: Web3Wrapper,
+ txDefaults: Partial<TxData>,
+): Promise<FillOrderCombinatorialUtils> {
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const userAddresses = _.slice(accounts, 0, 5);
+ const [ownerAddress, makerAddress, takerAddress] = userAddresses;
+ const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
+
+ const provider = web3Wrapper.getProvider();
+ const erc20Wrapper = new ERC20Wrapper(provider, userAddresses, ownerAddress);
+ const erc721Wrapper = new ERC721Wrapper(provider, userAddresses, ownerAddress);
+
+ const erc20EighteenDecimalTokenCount = 3;
+ const eighteenDecimals = new BigNumber(18);
+ const [
+ erc20EighteenDecimalTokenA,
+ erc20EighteenDecimalTokenB,
+ zrxToken,
+ ] = await erc20Wrapper.deployDummyTokensAsync(erc20EighteenDecimalTokenCount, eighteenDecimals);
+ const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+
+ const erc20FiveDecimalTokenCount = 2;
+ const fiveDecimals = new BigNumber(5);
+ const [erc20FiveDecimalTokenA, erc20FiveDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync(
+ erc20FiveDecimalTokenCount,
+ fiveDecimals,
+ );
+ const zeroDecimals = new BigNumber(0);
+ const erc20ZeroDecimalTokenCount = 2;
+ const [erc20ZeroDecimalTokenA, erc20ZeroDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync(
+ erc20ZeroDecimalTokenCount,
+ zeroDecimals,
+ );
+ const erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ const [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
+ const erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+
+ const assetWrapper = new AssetWrapper([erc20Wrapper, erc721Wrapper]);
+
+ const exchangeContract = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ zrxAssetData,
+ );
+ const exchangeWrapper = new ExchangeWrapper(exchangeContract, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, ownerAddress);
+ await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, ownerAddress);
+
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, {
+ from: ownerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, {
+ from: ownerAddress,
+ }),
+ constants.AWAIT_TRANSACTION_MINED_MS,
+ );
+
+ const orderFactory = new OrderFactoryFromScenario(
+ userAddresses,
+ zrxToken.address,
+ [erc20EighteenDecimalTokenA.address, erc20EighteenDecimalTokenB.address],
+ [erc20FiveDecimalTokenA.address, erc20FiveDecimalTokenB.address],
+ [erc20ZeroDecimalTokenA.address, erc20ZeroDecimalTokenB.address],
+ erc721Token,
+ erc721Balances,
+ exchangeContract.address,
+ );
+
+ const testLibsContract = await TestLibsContract.deployFrom0xArtifactAsync(
+ libsArtifacts.TestLibs,
+ provider,
+ txDefaults,
+ );
+
+ const fillOrderCombinatorialUtils = new FillOrderCombinatorialUtils(
+ orderFactory,
+ ownerAddress,
+ makerAddress,
+ makerPrivateKey,
+ takerAddress,
+ zrxAssetData,
+ exchangeWrapper,
+ assetWrapper,
+ testLibsContract,
+ );
+ return fillOrderCombinatorialUtils;
+}
+
+export class FillOrderCombinatorialUtils {
+ public orderFactory: OrderFactoryFromScenario;
+ public ownerAddress: string;
+ public makerAddress: string;
+ public makerPrivateKey: Buffer;
+ public takerAddress: string;
+ public zrxAssetData: string;
+ public exchangeWrapper: ExchangeWrapper;
+ public assetWrapper: AssetWrapper;
+ public testLibsContract: TestLibsContract;
+ public static generateFillOrderCombinations(): FillScenario[] {
+ const takerScenarios = [
+ TakerScenario.Unspecified,
+ // TakerScenario.CorrectlySpecified,
+ // TakerScenario.IncorrectlySpecified,
+ ];
+ const feeRecipientScenarios = [
+ FeeRecipientAddressScenario.EthUserAddress,
+ // FeeRecipientAddressScenario.BurnAddress,
+ ];
+ const makerAssetAmountScenario = [
+ OrderAssetAmountScenario.Large,
+ // OrderAssetAmountScenario.Zero,
+ // OrderAssetAmountScenario.Small,
+ ];
+ const takerAssetAmountScenario = [
+ OrderAssetAmountScenario.Large,
+ // OrderAssetAmountScenario.Zero,
+ // OrderAssetAmountScenario.Small,
+ ];
+ const makerFeeScenario = [
+ OrderAssetAmountScenario.Large,
+ // OrderAssetAmountScenario.Small,
+ // OrderAssetAmountScenario.Zero,
+ ];
+ const takerFeeScenario = [
+ OrderAssetAmountScenario.Large,
+ // OrderAssetAmountScenario.Small,
+ // OrderAssetAmountScenario.Zero,
+ ];
+ const expirationTimeSecondsScenario = [
+ ExpirationTimeSecondsScenario.InFuture,
+ ExpirationTimeSecondsScenario.InPast,
+ ];
+ const makerAssetDataScenario = [
+ AssetDataScenario.ERC20FiveDecimals,
+ AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ AssetDataScenario.ERC721,
+ AssetDataScenario.ZRXFeeToken,
+ ];
+ const takerAssetDataScenario = [
+ AssetDataScenario.ERC20FiveDecimals,
+ AssetDataScenario.ERC20NonZRXEighteenDecimals,
+ AssetDataScenario.ERC721,
+ AssetDataScenario.ZRXFeeToken,
+ ];
+ const takerAssetFillAmountScenario = [
+ TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
+ // TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
+ // TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount,
+ ];
+ const makerAssetBalanceScenario = [
+ BalanceAmountScenario.Higher,
+ // BalanceAmountScenario.Exact,
+ // BalanceAmountScenario.TooLow,
+ ];
+ const makerAssetAllowanceScenario = [
+ AllowanceAmountScenario.Higher,
+ // AllowanceAmountScenario.Exact,
+ // AllowanceAmountScenario.TooLow,
+ // AllowanceAmountScenario.Unlimited,
+ ];
+ const makerZRXBalanceScenario = [
+ BalanceAmountScenario.Higher,
+ // BalanceAmountScenario.Exact,
+ // BalanceAmountScenario.TooLow,
+ ];
+ const makerZRXAllowanceScenario = [
+ AllowanceAmountScenario.Higher,
+ // AllowanceAmountScenario.Exact,
+ // AllowanceAmountScenario.TooLow,
+ // AllowanceAmountScenario.Unlimited,
+ ];
+ const takerAssetBalanceScenario = [
+ BalanceAmountScenario.Higher,
+ // BalanceAmountScenario.Exact,
+ // BalanceAmountScenario.TooLow,
+ ];
+ const takerAssetAllowanceScenario = [
+ AllowanceAmountScenario.Higher,
+ // AllowanceAmountScenario.Exact,
+ // AllowanceAmountScenario.TooLow,
+ // AllowanceAmountScenario.Unlimited,
+ ];
+ const takerZRXBalanceScenario = [
+ BalanceAmountScenario.Higher,
+ // BalanceAmountScenario.Exact,
+ // BalanceAmountScenario.TooLow,
+ ];
+ const takerZRXAllowanceScenario = [
+ AllowanceAmountScenario.Higher,
+ // AllowanceAmountScenario.Exact,
+ // AllowanceAmountScenario.TooLow,
+ // AllowanceAmountScenario.Unlimited,
+ ];
+ const fillScenarioArrays = FillOrderCombinatorialUtils._getAllCombinations([
+ takerScenarios,
+ feeRecipientScenarios,
+ makerAssetAmountScenario,
+ takerAssetAmountScenario,
+ makerFeeScenario,
+ takerFeeScenario,
+ expirationTimeSecondsScenario,
+ makerAssetDataScenario,
+ takerAssetDataScenario,
+ takerAssetFillAmountScenario,
+ makerAssetBalanceScenario,
+ makerAssetAllowanceScenario,
+ makerZRXBalanceScenario,
+ makerZRXAllowanceScenario,
+ takerAssetBalanceScenario,
+ takerAssetAllowanceScenario,
+ takerZRXBalanceScenario,
+ takerZRXAllowanceScenario,
+ ]);
+
+ const fillScenarios = _.map(fillScenarioArrays, fillScenarioArray => {
+ // tslint:disable:custom-no-magic-numbers
+ const fillScenario: FillScenario = {
+ orderScenario: {
+ takerScenario: fillScenarioArray[0] as TakerScenario,
+ feeRecipientScenario: fillScenarioArray[1] as FeeRecipientAddressScenario,
+ makerAssetAmountScenario: fillScenarioArray[2] as OrderAssetAmountScenario,
+ takerAssetAmountScenario: fillScenarioArray[3] as OrderAssetAmountScenario,
+ makerFeeScenario: fillScenarioArray[4] as OrderAssetAmountScenario,
+ takerFeeScenario: fillScenarioArray[5] as OrderAssetAmountScenario,
+ expirationTimeSecondsScenario: fillScenarioArray[6] as ExpirationTimeSecondsScenario,
+ makerAssetDataScenario: fillScenarioArray[7] as AssetDataScenario,
+ takerAssetDataScenario: fillScenarioArray[8] as AssetDataScenario,
+ },
+ takerAssetFillAmountScenario: fillScenarioArray[9] as TakerAssetFillAmountScenario,
+ makerStateScenario: {
+ traderAssetBalance: fillScenarioArray[10] as BalanceAmountScenario,
+ traderAssetAllowance: fillScenarioArray[11] as AllowanceAmountScenario,
+ zrxFeeBalance: fillScenarioArray[12] as BalanceAmountScenario,
+ zrxFeeAllowance: fillScenarioArray[13] as AllowanceAmountScenario,
+ },
+ takerStateScenario: {
+ traderAssetBalance: fillScenarioArray[14] as BalanceAmountScenario,
+ traderAssetAllowance: fillScenarioArray[15] as AllowanceAmountScenario,
+ zrxFeeBalance: fillScenarioArray[16] as BalanceAmountScenario,
+ zrxFeeAllowance: fillScenarioArray[17] as AllowanceAmountScenario,
+ },
+ };
+ // tslint:enable:custom-no-magic-numbers
+ return fillScenario;
+ });
+
+ return fillScenarios;
+ }
+ /**
+ * Recursive implementation of generating all combinations of the supplied
+ * string-containing arrays.
+ */
+ private static _getAllCombinations(arrays: string[][]): string[][] {
+ // Base case
+ if (arrays.length === 1) {
+ const remainingValues = _.map(arrays[0], val => {
+ return [val];
+ });
+ return remainingValues;
+ } else {
+ const result = [];
+ const restOfArrays = arrays.slice(1);
+ const allCombinationsOfRemaining = FillOrderCombinatorialUtils._getAllCombinations(restOfArrays); // recur with the rest of array
+ // tslint:disable:prefer-for-of
+ for (let i = 0; i < allCombinationsOfRemaining.length; i++) {
+ for (let j = 0; j < arrays[0].length; j++) {
+ result.push([arrays[0][j], ...allCombinationsOfRemaining[i]]);
+ }
+ }
+ // tslint:enable:prefer-for-of
+ return result;
+ }
+ }
+ constructor(
+ orderFactory: OrderFactoryFromScenario,
+ ownerAddress: string,
+ makerAddress: string,
+ makerPrivateKey: Buffer,
+ takerAddress: string,
+ zrxAssetData: string,
+ exchangeWrapper: ExchangeWrapper,
+ assetWrapper: AssetWrapper,
+ testLibsContract: TestLibsContract,
+ ) {
+ this.orderFactory = orderFactory;
+ this.ownerAddress = ownerAddress;
+ this.makerAddress = makerAddress;
+ this.makerPrivateKey = makerPrivateKey;
+ this.takerAddress = takerAddress;
+ this.zrxAssetData = zrxAssetData;
+ this.exchangeWrapper = exchangeWrapper;
+ this.assetWrapper = assetWrapper;
+ this.testLibsContract = testLibsContract;
+ }
+ public async testFillOrderScenarioAsync(
+ provider: Provider,
+ fillScenario: FillScenario,
+ isVerbose: boolean = false,
+ ): Promise<void> {
+ // 1. Generate order
+ const order = this.orderFactory.generateOrder(fillScenario.orderScenario);
+
+ // 2. Sign order
+ const orderHashBuff = orderHashUtils.getOrderHashBuffer(order);
+ const signature = signingUtils.signMessage(orderHashBuff, this.makerPrivateKey, SignatureType.EthSign);
+ const signedOrder = {
+ ...order,
+ signature: `0x${signature.toString('hex')}`,
+ };
+
+ const balanceAndProxyAllowanceFetcher = new SimpleAssetBalanceAndProxyAllowanceFetcher(this.assetWrapper);
+ const orderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher(
+ this.exchangeWrapper,
+ this.zrxAssetData,
+ );
+
+ // 3. Figure out fill amount
+ const takerAssetFillAmount = await this._getTakerAssetFillAmountAsync(
+ signedOrder,
+ fillScenario.takerAssetFillAmountScenario,
+ balanceAndProxyAllowanceFetcher,
+ orderFilledCancelledFetcher,
+ );
+
+ // 4. Permutate the maker and taker balance/allowance scenarios
+ await this._modifyTraderStateAsync(
+ fillScenario.makerStateScenario,
+ fillScenario.takerStateScenario,
+ signedOrder,
+ takerAssetFillAmount,
+ );
+
+ // 5. If I fill it by X, what are the resulting balances/allowances/filled amounts expected?
+ const orderValidationUtils = new OrderValidationUtils(orderFilledCancelledFetcher, provider);
+ const lazyStore = new BalanceAndProxyAllowanceLazyStore(balanceAndProxyAllowanceFetcher);
+ const exchangeTransferSimulator = new ExchangeTransferSimulator(lazyStore);
+
+ let fillRevertReasonIfExists;
+ try {
+ await orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTransferSimulator,
+ provider,
+ signedOrder,
+ takerAssetFillAmount,
+ this.takerAddress,
+ this.zrxAssetData,
+ );
+ if (isVerbose) {
+ logUtils.log(`Expecting fillOrder to succeed.`);
+ }
+ } catch (err) {
+ fillRevertReasonIfExists = err.message;
+ if (isVerbose) {
+ logUtils.log(`Expecting fillOrder to fail with:`);
+ logUtils.log(err);
+ }
+ }
+
+ // 6. Fill the order
+ await this._fillOrderAndAssertOutcomeAsync(
+ signedOrder,
+ takerAssetFillAmount,
+ lazyStore,
+ fillRevertReasonIfExists,
+ );
+
+ await this._abiEncodeFillOrderAndAssertOutcomeAsync(signedOrder, takerAssetFillAmount);
+ }
+ private async _fillOrderAndAssertOutcomeAsync(
+ signedOrder: SignedOrder,
+ takerAssetFillAmount: BigNumber,
+ lazyStore: BalanceAndProxyAllowanceLazyStore,
+ fillRevertReasonIfExists: RevertReason | undefined,
+ ): Promise<void> {
+ if (!_.isUndefined(fillRevertReasonIfExists)) {
+ return expectTransactionFailedAsync(
+ this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { takerAssetFillAmount }),
+ fillRevertReasonIfExists,
+ );
+ }
+
+ const makerAddress = signedOrder.makerAddress;
+ const makerAssetData = signedOrder.makerAssetData;
+ const takerAssetData = signedOrder.takerAssetData;
+ const feeRecipient = signedOrder.feeRecipientAddress;
+
+ const expMakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(makerAssetData, makerAddress);
+ const expMakerAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(makerAssetData, makerAddress);
+ const expTakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(takerAssetData, makerAddress);
+ const expZRXAssetBalanceOfMaker = await lazyStore.getBalanceAsync(this.zrxAssetData, makerAddress);
+ const expZRXAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(this.zrxAssetData, makerAddress);
+ const expTakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(takerAssetData, this.takerAddress);
+ const expTakerAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(takerAssetData, this.takerAddress);
+ const expMakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(makerAssetData, this.takerAddress);
+ const expZRXAssetBalanceOfTaker = await lazyStore.getBalanceAsync(this.zrxAssetData, this.takerAddress);
+ const expZRXAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(
+ this.zrxAssetData,
+ this.takerAddress,
+ );
+ const expZRXAssetBalanceOfFeeRecipient = await lazyStore.getBalanceAsync(this.zrxAssetData, feeRecipient);
+
+ const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const alreadyFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash);
+ const remainingTakerAmountToFill = signedOrder.takerAssetAmount.minus(alreadyFilledTakerAmount);
+ const expFilledTakerAmount = takerAssetFillAmount.gt(remainingTakerAmountToFill)
+ ? remainingTakerAmountToFill
+ : alreadyFilledTakerAmount.add(takerAssetFillAmount);
+
+ const expFilledMakerAmount = orderUtils.getPartialAmountFloor(
+ expFilledTakerAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.makerAssetAmount,
+ );
+ const expMakerFeePaid = orderUtils.getPartialAmountFloor(
+ expFilledTakerAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.makerFee,
+ );
+ const expTakerFeePaid = orderUtils.getPartialAmountFloor(
+ expFilledTakerAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.takerFee,
+ );
+ const fillResults = await this.exchangeWrapper.getFillOrderResultsAsync(signedOrder, this.takerAddress, {
+ takerAssetFillAmount,
+ });
+ expect(fillResults.takerAssetFilledAmount).to.be.bignumber.equal(
+ expFilledTakerAmount,
+ 'takerAssetFilledAmount',
+ );
+ expect(fillResults.makerAssetFilledAmount).to.be.bignumber.equal(
+ expFilledMakerAmount,
+ 'makerAssetFilledAmount',
+ );
+ expect(fillResults.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'takerFeePaid');
+ expect(fillResults.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'makerFeePaid');
+
+ // - Let's fill the order!
+ const txReceipt = await this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, {
+ takerAssetFillAmount,
+ });
+
+ const actFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash);
+ expect(actFilledTakerAmount).to.be.bignumber.equal(expFilledTakerAmount, 'filledTakerAmount');
+
+ const exchangeLogs = _.filter(
+ txReceipt.logs,
+ txLog => txLog.address === this.exchangeWrapper.getExchangeAddress(),
+ );
+ expect(exchangeLogs.length).to.be.equal(1, 'logs length');
+ // tslint:disable-next-line:no-unnecessary-type-assertion
+ const log = txReceipt.logs[0] as LogWithDecodedArgs<ExchangeFillEventArgs>;
+ expect(log.args.makerAddress).to.be.equal(makerAddress, 'log.args.makerAddress');
+ expect(log.args.takerAddress).to.be.equal(this.takerAddress, 'log.args.this.takerAddress');
+ expect(log.args.feeRecipientAddress).to.be.equal(feeRecipient, 'log.args.feeRecipientAddress');
+ expect(log.args.makerAssetFilledAmount).to.be.bignumber.equal(
+ expFilledMakerAmount,
+ 'log.args.makerAssetFilledAmount',
+ );
+ expect(log.args.takerAssetFilledAmount).to.be.bignumber.equal(
+ expFilledTakerAmount,
+ 'log.args.takerAssetFilledAmount',
+ );
+ expect(log.args.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'log.args.makerFeePaid');
+ expect(log.args.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'logs.args.takerFeePaid');
+ expect(log.args.orderHash).to.be.equal(orderHash, 'log.args.orderHash');
+ expect(log.args.makerAssetData).to.be.equal(makerAssetData, 'log.args.makerAssetData');
+ expect(log.args.takerAssetData).to.be.equal(takerAssetData, 'log.args.takerAssetData');
+
+ const actMakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, makerAssetData);
+ expect(actMakerAssetBalanceOfMaker).to.be.bignumber.equal(
+ expMakerAssetBalanceOfMaker,
+ 'makerAssetBalanceOfMaker',
+ );
+
+ const actMakerAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync(
+ makerAddress,
+ makerAssetData,
+ );
+ expect(actMakerAssetAllowanceOfMaker).to.be.bignumber.equal(
+ expMakerAssetAllowanceOfMaker,
+ 'makerAssetAllowanceOfMaker',
+ );
+
+ const actTakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, takerAssetData);
+ expect(actTakerAssetBalanceOfMaker).to.be.bignumber.equal(
+ expTakerAssetBalanceOfMaker,
+ 'takerAssetBalanceOfMaker',
+ );
+
+ const actZRXAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, this.zrxAssetData);
+ expect(actZRXAssetBalanceOfMaker).to.be.bignumber.equal(expZRXAssetBalanceOfMaker, 'ZRXAssetBalanceOfMaker');
+
+ const actZRXAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync(
+ makerAddress,
+ this.zrxAssetData,
+ );
+ expect(actZRXAssetAllowanceOfMaker).to.be.bignumber.equal(
+ expZRXAssetAllowanceOfMaker,
+ 'ZRXAssetAllowanceOfMaker',
+ );
+
+ const actTakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, takerAssetData);
+ expect(actTakerAssetBalanceOfTaker).to.be.bignumber.equal(
+ expTakerAssetBalanceOfTaker,
+ 'TakerAssetBalanceOfTaker',
+ );
+
+ const actTakerAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync(
+ this.takerAddress,
+ takerAssetData,
+ );
+
+ expect(actTakerAssetAllowanceOfTaker).to.be.bignumber.equal(
+ expTakerAssetAllowanceOfTaker,
+ 'TakerAssetAllowanceOfTaker',
+ );
+
+ const actMakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, makerAssetData);
+ expect(actMakerAssetBalanceOfTaker).to.be.bignumber.equal(
+ expMakerAssetBalanceOfTaker,
+ 'MakerAssetBalanceOfTaker',
+ );
+
+ const actZRXAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, this.zrxAssetData);
+ expect(actZRXAssetBalanceOfTaker).to.be.bignumber.equal(expZRXAssetBalanceOfTaker, 'ZRXAssetBalanceOfTaker');
+
+ const actZRXAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync(
+ this.takerAddress,
+ this.zrxAssetData,
+ );
+ expect(actZRXAssetAllowanceOfTaker).to.be.bignumber.equal(
+ expZRXAssetAllowanceOfTaker,
+ 'ZRXAssetAllowanceOfTaker',
+ );
+
+ const actZRXAssetBalanceOfFeeRecipient = await this.assetWrapper.getBalanceAsync(
+ feeRecipient,
+ this.zrxAssetData,
+ );
+ expect(actZRXAssetBalanceOfFeeRecipient).to.be.bignumber.equal(
+ expZRXAssetBalanceOfFeeRecipient,
+ 'ZRXAssetBalanceOfFeeRecipient',
+ );
+ }
+ private async _abiEncodeFillOrderAndAssertOutcomeAsync(
+ signedOrder: SignedOrder,
+ takerAssetFillAmount: BigNumber,
+ ): Promise<void> {
+ const params = orderUtils.createFill(signedOrder, takerAssetFillAmount);
+ const expectedAbiEncodedData = this.exchangeWrapper.abiEncodeFillOrder(signedOrder, { takerAssetFillAmount });
+ const libsAbiEncodedData = await this.testLibsContract.publicAbiEncodeFillOrder.callAsync(
+ params.order,
+ params.takerAssetFillAmount,
+ params.signature,
+ );
+ expect(libsAbiEncodedData).to.be.equal(expectedAbiEncodedData, 'ABIEncodedFillOrderData');
+ }
+ private async _getTakerAssetFillAmountAsync(
+ signedOrder: SignedOrder,
+ takerAssetFillAmountScenario: TakerAssetFillAmountScenario,
+ balanceAndProxyAllowanceFetcher: SimpleAssetBalanceAndProxyAllowanceFetcher,
+ orderFilledCancelledFetcher: SimpleOrderFilledCancelledFetcher,
+ ): Promise<BigNumber> {
+ const orderStateUtils = new OrderStateUtils(balanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher);
+ const fillableTakerAssetAmount = await orderStateUtils.getMaxFillableTakerAssetAmountAsync(
+ signedOrder,
+ this.takerAddress,
+ );
+
+ let takerAssetFillAmount;
+ switch (takerAssetFillAmountScenario) {
+ case TakerAssetFillAmountScenario.Zero:
+ takerAssetFillAmount = new BigNumber(0);
+ break;
+
+ case TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount:
+ takerAssetFillAmount = fillableTakerAssetAmount;
+ break;
+
+ case TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount:
+ takerAssetFillAmount = fillableTakerAssetAmount.add(1);
+ break;
+
+ case TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount:
+ const takerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.takerAssetData);
+ const makerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.makerAssetData);
+ const isEitherAssetERC721 =
+ takerAssetProxyId === AssetProxyId.ERC721 || makerAssetProxyId === AssetProxyId.ERC721;
+ if (isEitherAssetERC721) {
+ throw new Error(
+ 'Cannot test `TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount` together with ERC721 assets since orders involving ERC721 must always be filled exactly.',
+ );
+ }
+ takerAssetFillAmount = fillableTakerAssetAmount.div(2).floor();
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr('TakerAssetFillAmountScenario', takerAssetFillAmountScenario);
+ }
+
+ return takerAssetFillAmount;
+ }
+ private async _modifyTraderStateAsync(
+ makerStateScenario: TraderStateScenario,
+ takerStateScenario: TraderStateScenario,
+ signedOrder: SignedOrder,
+ takerAssetFillAmount: BigNumber,
+ ): Promise<void> {
+ const makerAssetFillAmount = orderUtils.getPartialAmountFloor(
+ takerAssetFillAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.makerAssetAmount,
+ );
+ switch (makerStateScenario.traderAssetBalance) {
+ case BalanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case BalanceAmountScenario.TooLow:
+ if (makerAssetFillAmount.eq(0)) {
+ throw new Error(`Cannot set makerAssetBalanceOfMaker TooLow if makerAssetFillAmount is 0`);
+ }
+ const tooLowBalance = makerAssetFillAmount.minus(1);
+ await this.assetWrapper.setBalanceAsync(
+ signedOrder.makerAddress,
+ signedOrder.makerAssetData,
+ tooLowBalance,
+ );
+ break;
+
+ case BalanceAmountScenario.Exact:
+ const exactBalance = makerAssetFillAmount;
+ await this.assetWrapper.setBalanceAsync(
+ signedOrder.makerAddress,
+ signedOrder.makerAssetData,
+ exactBalance,
+ );
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'makerStateScenario.traderAssetBalance',
+ makerStateScenario.traderAssetBalance,
+ );
+ }
+
+ const makerFee = orderUtils.getPartialAmountFloor(
+ takerAssetFillAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.makerFee,
+ );
+ switch (makerStateScenario.zrxFeeBalance) {
+ case BalanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case BalanceAmountScenario.TooLow:
+ if (makerFee.eq(0)) {
+ throw new Error(`Cannot set zrxAsserBalanceOfMaker TooLow if makerFee is 0`);
+ }
+ const tooLowBalance = makerFee.minus(1);
+ await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, tooLowBalance);
+ break;
+
+ case BalanceAmountScenario.Exact:
+ const exactBalance = makerFee;
+ await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, exactBalance);
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr('makerStateScenario.zrxFeeBalance', makerStateScenario.zrxFeeBalance);
+ }
+
+ switch (makerStateScenario.traderAssetAllowance) {
+ case AllowanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case AllowanceAmountScenario.TooLow:
+ const tooLowAllowance = makerAssetFillAmount.minus(1);
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ signedOrder.makerAssetData,
+ tooLowAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Exact:
+ const exactAllowance = makerAssetFillAmount;
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ signedOrder.makerAssetData,
+ exactAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Unlimited:
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ signedOrder.makerAssetData,
+ constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ );
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'makerStateScenario.traderAssetAllowance',
+ makerStateScenario.traderAssetAllowance,
+ );
+ }
+
+ switch (makerStateScenario.zrxFeeAllowance) {
+ case AllowanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case AllowanceAmountScenario.TooLow:
+ const tooLowAllowance = makerFee.minus(1);
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ this.zrxAssetData,
+ tooLowAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Exact:
+ const exactAllowance = makerFee;
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ this.zrxAssetData,
+ exactAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Unlimited:
+ await this.assetWrapper.setProxyAllowanceAsync(
+ signedOrder.makerAddress,
+ this.zrxAssetData,
+ constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ );
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'makerStateScenario.zrxFeeAllowance',
+ makerStateScenario.zrxFeeAllowance,
+ );
+ }
+
+ switch (takerStateScenario.traderAssetBalance) {
+ case BalanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case BalanceAmountScenario.TooLow:
+ if (takerAssetFillAmount.eq(0)) {
+ throw new Error(`Cannot set takerAssetBalanceOfTaker TooLow if takerAssetFillAmount is 0`);
+ }
+ const tooLowBalance = takerAssetFillAmount.minus(1);
+ await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, tooLowBalance);
+ break;
+
+ case BalanceAmountScenario.Exact:
+ const exactBalance = takerAssetFillAmount;
+ await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, exactBalance);
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'takerStateScenario.traderAssetBalance',
+ takerStateScenario.traderAssetBalance,
+ );
+ }
+
+ const takerFee = orderUtils.getPartialAmountFloor(
+ takerAssetFillAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.takerFee,
+ );
+ switch (takerStateScenario.zrxFeeBalance) {
+ case BalanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case BalanceAmountScenario.TooLow:
+ if (takerFee.eq(0)) {
+ throw new Error(`Cannot set zrxAssetBalanceOfTaker TooLow if takerFee is 0`);
+ }
+ const tooLowBalance = takerFee.minus(1);
+ await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, tooLowBalance);
+ break;
+
+ case BalanceAmountScenario.Exact:
+ const exactBalance = takerFee;
+ await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, exactBalance);
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr('takerStateScenario.zrxFeeBalance', takerStateScenario.zrxFeeBalance);
+ }
+
+ switch (takerStateScenario.traderAssetAllowance) {
+ case AllowanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case AllowanceAmountScenario.TooLow:
+ const tooLowAllowance = takerAssetFillAmount.minus(1);
+ await this.assetWrapper.setProxyAllowanceAsync(
+ this.takerAddress,
+ signedOrder.takerAssetData,
+ tooLowAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Exact:
+ const exactAllowance = takerAssetFillAmount;
+ await this.assetWrapper.setProxyAllowanceAsync(
+ this.takerAddress,
+ signedOrder.takerAssetData,
+ exactAllowance,
+ );
+ break;
+
+ case AllowanceAmountScenario.Unlimited:
+ await this.assetWrapper.setProxyAllowanceAsync(
+ this.takerAddress,
+ signedOrder.takerAssetData,
+ constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ );
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'takerStateScenario.traderAssetAllowance',
+ takerStateScenario.traderAssetAllowance,
+ );
+ }
+
+ switch (takerStateScenario.zrxFeeAllowance) {
+ case AllowanceAmountScenario.Higher:
+ break; // Noop since this is already the default
+
+ case AllowanceAmountScenario.TooLow:
+ const tooLowAllowance = takerFee.minus(1);
+ await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, tooLowAllowance);
+ break;
+
+ case AllowanceAmountScenario.Exact:
+ const exactAllowance = takerFee;
+ await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, exactAllowance);
+ break;
+
+ case AllowanceAmountScenario.Unlimited:
+ await this.assetWrapper.setProxyAllowanceAsync(
+ this.takerAddress,
+ this.zrxAssetData,
+ constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ );
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'takerStateScenario.zrxFeeAllowance',
+ takerStateScenario.zrxFeeAllowance,
+ );
+ }
+ }
+} // tslint:disable:max-file-line-count
diff --git a/contracts/protocol/test/utils/index.ts b/contracts/protocol/test/utils/index.ts
new file mode 100644
index 000000000..75cd88666
--- /dev/null
+++ b/contracts/protocol/test/utils/index.ts
@@ -0,0 +1,3 @@
+export * from './exchange_wrapper';
+export * from './erc20_wrapper';
+export * from './erc721_wrapper';
diff --git a/contracts/protocol/test/utils/match_order_tester.ts b/contracts/protocol/test/utils/match_order_tester.ts
new file mode 100644
index 000000000..8f574704e
--- /dev/null
+++ b/contracts/protocol/test/utils/match_order_tester.ts
@@ -0,0 +1,562 @@
+import {
+ chaiSetup,
+ ERC20BalancesByOwner,
+ ERC721TokenIdsByOwner,
+ OrderInfo,
+ OrderStatus,
+ TransferAmountsByMatchOrders as TransferAmounts,
+ TransferAmountsLoggedByMatchOrders as LoggedTransferAmounts,
+} from '@0x/contracts-test-utils';
+import { assetDataUtils, orderHashUtils } from '@0x/order-utils';
+import { AssetProxyId, SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+
+import { TransactionReceiptWithDecodedLogs } from '../../../../node_modules/ethereum-types';
+
+import { ERC20Wrapper } from './erc20_wrapper';
+import { ERC721Wrapper } from './erc721_wrapper';
+import { ExchangeWrapper } from './exchange_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+export class MatchOrderTester {
+ private readonly _exchangeWrapper: ExchangeWrapper;
+ private readonly _erc20Wrapper: ERC20Wrapper;
+ private readonly _erc721Wrapper: ERC721Wrapper;
+ private readonly _feeTokenAddress: string;
+ /// @dev Checks values from the logs produced by Exchange.matchOrders against the expected transfer amounts.
+ /// Values include the amounts transferred from the left/right makers and taker, along with
+ /// the fees paid on each matched order. These are also the return values of MatchOrders.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param transactionReceipt Transaction receipt and logs produced by Exchange.matchOrders.
+ /// @param takerAddress Address of taker (account that called Exchange.matchOrders)
+ /// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
+ private static async _assertLogsAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ transactionReceipt: TransactionReceiptWithDecodedLogs,
+ takerAddress: string,
+ expectedTransferAmounts: TransferAmounts,
+ ): Promise<void> {
+ // Should have two fill event logs -- one for each order.
+ const transactionFillLogs = _.filter(transactionReceipt.logs, ['event', 'Fill']);
+ expect(transactionFillLogs.length, 'Checking number of logs').to.be.equal(2);
+ // First log is for left fill
+ const leftLog = (transactionFillLogs[0] as any).args as LoggedTransferAmounts;
+ expect(leftLog.makerAddress, 'Checking logged maker address of left order').to.be.equal(
+ signedOrderLeft.makerAddress,
+ );
+ expect(leftLog.takerAddress, 'Checking logged taker address of right order').to.be.equal(takerAddress);
+ const amountBoughtByLeftMaker = new BigNumber(leftLog.takerAssetFilledAmount);
+ const amountSoldByLeftMaker = new BigNumber(leftLog.makerAssetFilledAmount);
+ const feePaidByLeftMaker = new BigNumber(leftLog.makerFeePaid);
+ const feePaidByTakerLeft = new BigNumber(leftLog.takerFeePaid);
+ // Second log is for right fill
+ const rightLog = (transactionFillLogs[1] as any).args as LoggedTransferAmounts;
+ expect(rightLog.makerAddress, 'Checking logged maker address of right order').to.be.equal(
+ signedOrderRight.makerAddress,
+ );
+ expect(rightLog.takerAddress, 'Checking loggerd taker address of right order').to.be.equal(takerAddress);
+ const amountBoughtByRightMaker = new BigNumber(rightLog.takerAssetFilledAmount);
+ const amountSoldByRightMaker = new BigNumber(rightLog.makerAssetFilledAmount);
+ const feePaidByRightMaker = new BigNumber(rightLog.makerFeePaid);
+ const feePaidByTakerRight = new BigNumber(rightLog.takerFeePaid);
+ // Derive amount received by taker
+ const amountReceivedByTaker = amountSoldByLeftMaker.sub(amountBoughtByRightMaker);
+ // Assert log values - left order
+ expect(amountBoughtByLeftMaker, 'Checking logged amount bought by left maker').to.be.bignumber.equal(
+ expectedTransferAmounts.amountBoughtByLeftMaker,
+ );
+ expect(amountSoldByLeftMaker, 'Checking logged amount sold by left maker').to.be.bignumber.equal(
+ expectedTransferAmounts.amountSoldByLeftMaker,
+ );
+ expect(feePaidByLeftMaker, 'Checking logged fee paid by left maker').to.be.bignumber.equal(
+ expectedTransferAmounts.feePaidByLeftMaker,
+ );
+ expect(feePaidByTakerLeft, 'Checking logged fee paid on left order by taker').to.be.bignumber.equal(
+ expectedTransferAmounts.feePaidByTakerLeft,
+ );
+ // Assert log values - right order
+ expect(amountBoughtByRightMaker, 'Checking logged amount bought by right maker').to.be.bignumber.equal(
+ expectedTransferAmounts.amountBoughtByRightMaker,
+ );
+ expect(amountSoldByRightMaker, 'Checking logged amount sold by right maker').to.be.bignumber.equal(
+ expectedTransferAmounts.amountSoldByRightMaker,
+ );
+ expect(feePaidByRightMaker, 'Checking logged fee paid by right maker').to.be.bignumber.equal(
+ expectedTransferAmounts.feePaidByRightMaker,
+ );
+ expect(feePaidByTakerRight, 'Checking logged fee paid on right order by taker').to.be.bignumber.equal(
+ expectedTransferAmounts.feePaidByTakerRight,
+ );
+ // Assert derived amount received by taker
+ expect(amountReceivedByTaker, 'Checking logged amount received by taker').to.be.bignumber.equal(
+ expectedTransferAmounts.amountReceivedByTaker,
+ );
+ }
+ /// @dev Asserts all expected ERC20 and ERC721 account holdings match the real holdings.
+ /// @param expectedERC20BalancesByOwner Expected ERC20 balances.
+ /// @param realERC20BalancesByOwner Real ERC20 balances.
+ /// @param expectedERC721TokenIdsByOwner Expected ERC721 token owners.
+ /// @param realERC721TokenIdsByOwner Real ERC20 token owners.
+ private static async _assertAllKnownBalancesAsync(
+ expectedERC20BalancesByOwner: ERC20BalancesByOwner,
+ realERC20BalancesByOwner: ERC20BalancesByOwner,
+ expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ realERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ ): Promise<void> {
+ // ERC20 Balances
+ const areERC20BalancesEqual = _.isEqual(expectedERC20BalancesByOwner, realERC20BalancesByOwner);
+ expect(areERC20BalancesEqual, 'Checking all known ERC20 account balances').to.be.true();
+ // ERC721 Token Ids
+ const sortedExpectedNewERC721TokenIdsByOwner = _.mapValues(expectedERC721TokenIdsByOwner, tokenIdsByOwner => {
+ _.mapValues(tokenIdsByOwner, tokenIds => {
+ _.sortBy(tokenIds);
+ });
+ });
+ const sortedNewERC721TokenIdsByOwner = _.mapValues(realERC721TokenIdsByOwner, tokenIdsByOwner => {
+ _.mapValues(tokenIdsByOwner, tokenIds => {
+ _.sortBy(tokenIds);
+ });
+ });
+ const areERC721TokenIdsEqual = _.isEqual(
+ sortedExpectedNewERC721TokenIdsByOwner,
+ sortedNewERC721TokenIdsByOwner,
+ );
+ expect(areERC721TokenIdsEqual, 'Checking all known ERC721 account balances').to.be.true();
+ }
+ /// @dev Constructs new MatchOrderTester.
+ /// @param exchangeWrapper Used to call to the Exchange.
+ /// @param erc20Wrapper Used to fetch ERC20 balances.
+ /// @param erc721Wrapper Used to fetch ERC721 token owners.
+ /// @param feeTokenAddress Address of ERC20 fee token.
+ constructor(
+ exchangeWrapper: ExchangeWrapper,
+ erc20Wrapper: ERC20Wrapper,
+ erc721Wrapper: ERC721Wrapper,
+ feeTokenAddress: string,
+ ) {
+ this._exchangeWrapper = exchangeWrapper;
+ this._erc20Wrapper = erc20Wrapper;
+ this._erc721Wrapper = erc721Wrapper;
+ this._feeTokenAddress = feeTokenAddress;
+ }
+ /// @dev Matches two complementary orders and asserts results.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param takerAddress Address of taker (the address who matched the two orders)
+ /// @param erc20BalancesByOwner Current ERC20 balances.
+ /// @param erc721TokenIdsByOwner Current ERC721 token owners.
+ /// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
+ /// @param initialLeftOrderFilledAmount How much left order has been filled, prior to matching orders.
+ /// @param initialRightOrderFilledAmount How much the right order has been filled, prior to matching orders.
+ /// @return New ERC20 balances & ERC721 token owners.
+ public async matchOrdersAndAssertEffectsAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ takerAddress: string,
+ erc20BalancesByOwner: ERC20BalancesByOwner,
+ erc721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ expectedTransferAmounts: TransferAmounts,
+ initialLeftOrderFilledAmount: BigNumber = new BigNumber(0),
+ initialRightOrderFilledAmount: BigNumber = new BigNumber(0),
+ ): Promise<[ERC20BalancesByOwner, ERC721TokenIdsByOwner]> {
+ // Assert initial order states
+ await this._assertInitialOrderStatesAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ initialLeftOrderFilledAmount,
+ initialRightOrderFilledAmount,
+ );
+ // Match left & right orders
+ const transactionReceipt = await this._exchangeWrapper.matchOrdersAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ );
+ const newERC20BalancesByOwner = await this._erc20Wrapper.getBalancesAsync();
+ const newERC721TokenIdsByOwner = await this._erc721Wrapper.getBalancesAsync();
+ // Assert logs
+ await MatchOrderTester._assertLogsAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ transactionReceipt,
+ takerAddress,
+ expectedTransferAmounts,
+ );
+ // Assert exchange state
+ await this._assertExchangeStateAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ initialLeftOrderFilledAmount,
+ initialRightOrderFilledAmount,
+ expectedTransferAmounts,
+ );
+ // Assert balances of makers, taker, and fee recipients
+ await this._assertBalancesAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ erc20BalancesByOwner,
+ erc721TokenIdsByOwner,
+ newERC20BalancesByOwner,
+ newERC721TokenIdsByOwner,
+ expectedTransferAmounts,
+ takerAddress,
+ );
+ return [newERC20BalancesByOwner, newERC721TokenIdsByOwner];
+ }
+ /// @dev Asserts initial exchange state for the left and right orders.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param expectedOrderFilledAmountLeft How much left order has been filled, prior to matching orders.
+ /// @param expectedOrderFilledAmountRight How much the right order has been filled, prior to matching orders.
+ private async _assertInitialOrderStatesAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ expectedOrderFilledAmountLeft: BigNumber,
+ expectedOrderFilledAmountRight: BigNumber,
+ ): Promise<void> {
+ // Assert left order initial state
+ const orderTakerAssetFilledAmountLeft = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
+ orderHashUtils.getOrderHashHex(signedOrderLeft),
+ );
+ expect(orderTakerAssetFilledAmountLeft, 'Checking inital state of left order').to.be.bignumber.equal(
+ expectedOrderFilledAmountLeft,
+ );
+ // Assert right order initial state
+ const orderTakerAssetFilledAmountRight = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
+ orderHashUtils.getOrderHashHex(signedOrderRight),
+ );
+ expect(orderTakerAssetFilledAmountRight, 'Checking inital state of right order').to.be.bignumber.equal(
+ expectedOrderFilledAmountRight,
+ );
+ }
+ /// @dev Asserts the exchange state against the expected amounts transferred by from matching orders.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param initialLeftOrderFilledAmount How much left order has been filled, prior to matching orders.
+ /// @param initialRightOrderFilledAmount How much the right order has been filled, prior to matching orders.
+ /// @return TransferAmounts A struct containing the expected transfer amounts.
+ private async _assertExchangeStateAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ initialLeftOrderFilledAmount: BigNumber,
+ initialRightOrderFilledAmount: BigNumber,
+ expectedTransferAmounts: TransferAmounts,
+ ): Promise<void> {
+ // Assert state for left order: amount bought by left maker
+ let amountBoughtByLeftMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
+ orderHashUtils.getOrderHashHex(signedOrderLeft),
+ );
+ amountBoughtByLeftMaker = amountBoughtByLeftMaker.minus(initialLeftOrderFilledAmount);
+ expect(amountBoughtByLeftMaker, 'Checking exchange state for left order').to.be.bignumber.equal(
+ expectedTransferAmounts.amountBoughtByLeftMaker,
+ );
+ // Assert state for right order: amount bought by right maker
+ let amountBoughtByRightMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync(
+ orderHashUtils.getOrderHashHex(signedOrderRight),
+ );
+ amountBoughtByRightMaker = amountBoughtByRightMaker.minus(initialRightOrderFilledAmount);
+ expect(amountBoughtByRightMaker, 'Checking exchange state for right order').to.be.bignumber.equal(
+ expectedTransferAmounts.amountBoughtByRightMaker,
+ );
+ // Assert left order status
+ const maxAmountBoughtByLeftMaker = signedOrderLeft.takerAssetAmount.minus(initialLeftOrderFilledAmount);
+ const leftOrderInfo: OrderInfo = await this._exchangeWrapper.getOrderInfoAsync(signedOrderLeft);
+ const leftExpectedStatus = expectedTransferAmounts.amountBoughtByLeftMaker.equals(maxAmountBoughtByLeftMaker)
+ ? OrderStatus.FULLY_FILLED
+ : OrderStatus.FILLABLE;
+ expect(leftOrderInfo.orderStatus, 'Checking exchange status for left order').to.be.equal(leftExpectedStatus);
+ // Assert right order status
+ const maxAmountBoughtByRightMaker = signedOrderRight.takerAssetAmount.minus(initialRightOrderFilledAmount);
+ const rightOrderInfo: OrderInfo = await this._exchangeWrapper.getOrderInfoAsync(signedOrderRight);
+ const rightExpectedStatus = expectedTransferAmounts.amountBoughtByRightMaker.equals(maxAmountBoughtByRightMaker)
+ ? OrderStatus.FULLY_FILLED
+ : OrderStatus.FILLABLE;
+ expect(rightOrderInfo.orderStatus, 'Checking exchange status for right order').to.be.equal(rightExpectedStatus);
+ }
+ /// @dev Asserts account balances after matching orders.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param initialERC20BalancesByOwner ERC20 balances prior to order matching.
+ /// @param initialERC721TokenIdsByOwner ERC721 token owners prior to order matching.
+ /// @param finalERC20BalancesByOwner ERC20 balances after order matching.
+ /// @param finalERC721TokenIdsByOwner ERC721 token owners after order matching.
+ /// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
+ /// @param takerAddress Address of taker (account that called Exchange.matchOrders).
+ private async _assertBalancesAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ initialERC20BalancesByOwner: ERC20BalancesByOwner,
+ initialERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ finalERC20BalancesByOwner: ERC20BalancesByOwner,
+ finalERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ expectedTransferAmounts: TransferAmounts,
+ takerAddress: string,
+ ): Promise<void> {
+ let expectedERC20BalancesByOwner: ERC20BalancesByOwner;
+ let expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner;
+ [expectedERC20BalancesByOwner, expectedERC721TokenIdsByOwner] = this._calculateExpectedBalances(
+ signedOrderLeft,
+ signedOrderRight,
+ takerAddress,
+ initialERC20BalancesByOwner,
+ initialERC721TokenIdsByOwner,
+ expectedTransferAmounts,
+ );
+ // Assert balances of makers, taker, and fee recipients
+ await this._assertMakerTakerAndFeeRecipientBalancesAsync(
+ signedOrderLeft,
+ signedOrderRight,
+ expectedERC20BalancesByOwner,
+ finalERC20BalancesByOwner,
+ expectedERC721TokenIdsByOwner,
+ finalERC721TokenIdsByOwner,
+ takerAddress,
+ );
+ // Assert balances for all known accounts
+ await MatchOrderTester._assertAllKnownBalancesAsync(
+ expectedERC20BalancesByOwner,
+ finalERC20BalancesByOwner,
+ expectedERC721TokenIdsByOwner,
+ finalERC721TokenIdsByOwner,
+ );
+ }
+ /// @dev Calculates the expected balances of order makers, fee recipients, and the taker,
+ /// as a result of matching two orders.
+ /// @param signedOrderRight First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param takerAddress Address of taker (the address who matched the two orders)
+ /// @param erc20BalancesByOwner Current ERC20 balances.
+ /// @param erc721TokenIdsByOwner Current ERC721 token owners.
+ /// @param expectedTransferAmounts Expected amounts transferred as a result of order matching.
+ /// @return Expected ERC20 balances & ERC721 token owners after orders have been matched.
+ private _calculateExpectedBalances(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ takerAddress: string,
+ erc20BalancesByOwner: ERC20BalancesByOwner,
+ erc721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ expectedTransferAmounts: TransferAmounts,
+ ): [ERC20BalancesByOwner, ERC721TokenIdsByOwner] {
+ const makerAddressLeft = signedOrderLeft.makerAddress;
+ const makerAddressRight = signedOrderRight.makerAddress;
+ const feeRecipientAddressLeft = signedOrderLeft.feeRecipientAddress;
+ const feeRecipientAddressRight = signedOrderRight.feeRecipientAddress;
+ // Operations are performed on copies of the balances
+ const expectedNewERC20BalancesByOwner = _.cloneDeep(erc20BalancesByOwner);
+ const expectedNewERC721TokenIdsByOwner = _.cloneDeep(erc721TokenIdsByOwner);
+ // Left Maker Asset (Right Taker Asset)
+ const makerAssetProxyIdLeft = assetDataUtils.decodeAssetProxyId(signedOrderLeft.makerAssetData);
+ if (makerAssetProxyIdLeft === AssetProxyId.ERC20) {
+ // Decode asset data
+ const erc20AssetData = assetDataUtils.decodeERC20AssetData(signedOrderLeft.makerAssetData);
+ const makerAssetAddressLeft = erc20AssetData.tokenAddress;
+ const takerAssetAddressRight = makerAssetAddressLeft;
+ // Left Maker
+ expectedNewERC20BalancesByOwner[makerAddressLeft][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[
+ makerAddressLeft
+ ][makerAssetAddressLeft].minus(expectedTransferAmounts.amountSoldByLeftMaker);
+ // Right Maker
+ expectedNewERC20BalancesByOwner[makerAddressRight][
+ takerAssetAddressRight
+ ] = expectedNewERC20BalancesByOwner[makerAddressRight][takerAssetAddressRight].add(
+ expectedTransferAmounts.amountBoughtByRightMaker,
+ );
+ // Taker
+ expectedNewERC20BalancesByOwner[takerAddress][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[
+ takerAddress
+ ][makerAssetAddressLeft].add(expectedTransferAmounts.amountReceivedByTaker);
+ } else if (makerAssetProxyIdLeft === AssetProxyId.ERC721) {
+ // Decode asset data
+ const erc721AssetData = assetDataUtils.decodeERC721AssetData(signedOrderLeft.makerAssetData);
+ const makerAssetAddressLeft = erc721AssetData.tokenAddress;
+ const makerAssetIdLeft = erc721AssetData.tokenId;
+ const takerAssetAddressRight = makerAssetAddressLeft;
+ const takerAssetIdRight = makerAssetIdLeft;
+ // Left Maker
+ _.remove(expectedNewERC721TokenIdsByOwner[makerAddressLeft][makerAssetAddressLeft], makerAssetIdLeft);
+ // Right Maker
+ expectedNewERC721TokenIdsByOwner[makerAddressRight][takerAssetAddressRight].push(takerAssetIdRight);
+ // Taker: Since there is only 1 asset transferred, the taker does not receive any of the left maker asset.
+ }
+ // Left Taker Asset (Right Maker Asset)
+ // Note: This exchange is only between the order makers: the Taker does not receive any of the left taker asset.
+ const takerAssetProxyIdLeft = assetDataUtils.decodeAssetProxyId(signedOrderLeft.takerAssetData);
+ if (takerAssetProxyIdLeft === AssetProxyId.ERC20) {
+ // Decode asset data
+ const erc20AssetData = assetDataUtils.decodeERC20AssetData(signedOrderLeft.takerAssetData);
+ const takerAssetAddressLeft = erc20AssetData.tokenAddress;
+ const makerAssetAddressRight = takerAssetAddressLeft;
+ // Left Maker
+ expectedNewERC20BalancesByOwner[makerAddressLeft][takerAssetAddressLeft] = expectedNewERC20BalancesByOwner[
+ makerAddressLeft
+ ][takerAssetAddressLeft].add(expectedTransferAmounts.amountBoughtByLeftMaker);
+ // Right Maker
+ expectedNewERC20BalancesByOwner[makerAddressRight][
+ makerAssetAddressRight
+ ] = expectedNewERC20BalancesByOwner[makerAddressRight][makerAssetAddressRight].minus(
+ expectedTransferAmounts.amountSoldByRightMaker,
+ );
+ } else if (takerAssetProxyIdLeft === AssetProxyId.ERC721) {
+ // Decode asset data
+ const erc721AssetData = assetDataUtils.decodeERC721AssetData(signedOrderRight.makerAssetData);
+ const makerAssetAddressRight = erc721AssetData.tokenAddress;
+ const makerAssetIdRight = erc721AssetData.tokenId;
+ const takerAssetAddressLeft = makerAssetAddressRight;
+ const takerAssetIdLeft = makerAssetIdRight;
+ // Right Maker
+ _.remove(expectedNewERC721TokenIdsByOwner[makerAddressRight][makerAssetAddressRight], makerAssetIdRight);
+ // Left Maker
+ expectedNewERC721TokenIdsByOwner[makerAddressLeft][takerAssetAddressLeft].push(takerAssetIdLeft);
+ }
+ // Left Maker Fees
+ expectedNewERC20BalancesByOwner[makerAddressLeft][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[
+ makerAddressLeft
+ ][this._feeTokenAddress].minus(expectedTransferAmounts.feePaidByLeftMaker);
+ // Right Maker Fees
+ expectedNewERC20BalancesByOwner[makerAddressRight][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[
+ makerAddressRight
+ ][this._feeTokenAddress].minus(expectedTransferAmounts.feePaidByRightMaker);
+ // Taker Fees
+ expectedNewERC20BalancesByOwner[takerAddress][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[
+ takerAddress
+ ][this._feeTokenAddress].minus(
+ expectedTransferAmounts.feePaidByTakerLeft.add(expectedTransferAmounts.feePaidByTakerRight),
+ );
+ // Left Fee Recipient Fees
+ expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][
+ this._feeTokenAddress
+ ] = expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][this._feeTokenAddress].add(
+ expectedTransferAmounts.feePaidByLeftMaker.add(expectedTransferAmounts.feePaidByTakerLeft),
+ );
+ // Right Fee Recipient Fees
+ expectedNewERC20BalancesByOwner[feeRecipientAddressRight][
+ this._feeTokenAddress
+ ] = expectedNewERC20BalancesByOwner[feeRecipientAddressRight][this._feeTokenAddress].add(
+ expectedTransferAmounts.feePaidByRightMaker.add(expectedTransferAmounts.feePaidByTakerRight),
+ );
+
+ return [expectedNewERC20BalancesByOwner, expectedNewERC721TokenIdsByOwner];
+ }
+ /// @dev Asserts ERC20 account balances and ERC721 token holdings that result from order matching.
+ /// Specifically checks balances of makers, taker and fee recipients.
+ /// @param signedOrderLeft First matched order.
+ /// @param signedOrderRight Second matched order.
+ /// @param expectedERC20BalancesByOwner Expected ERC20 balances.
+ /// @param realERC20BalancesByOwner Real ERC20 balances.
+ /// @param expectedERC721TokenIdsByOwner Expected ERC721 token owners.
+ /// @param realERC721TokenIdsByOwner Real ERC20 token owners.
+ /// @param takerAddress Address of taker (account that called Exchange.matchOrders).
+ private async _assertMakerTakerAndFeeRecipientBalancesAsync(
+ signedOrderLeft: SignedOrder,
+ signedOrderRight: SignedOrder,
+ expectedERC20BalancesByOwner: ERC20BalancesByOwner,
+ realERC20BalancesByOwner: ERC20BalancesByOwner,
+ expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ realERC721TokenIdsByOwner: ERC721TokenIdsByOwner,
+ takerAddress: string,
+ ): Promise<void> {
+ // Individual balance comparisons
+ const makerAssetProxyIdLeft = assetDataUtils.decodeAssetProxyId(signedOrderLeft.makerAssetData);
+ const makerERC20AssetDataLeft =
+ makerAssetProxyIdLeft === AssetProxyId.ERC20
+ ? assetDataUtils.decodeERC20AssetData(signedOrderLeft.makerAssetData)
+ : assetDataUtils.decodeERC721AssetData(signedOrderLeft.makerAssetData);
+ const makerAssetAddressLeft = makerERC20AssetDataLeft.tokenAddress;
+ const makerAssetProxyIdRight = assetDataUtils.decodeAssetProxyId(signedOrderRight.makerAssetData);
+ const makerERC20AssetDataRight =
+ makerAssetProxyIdRight === AssetProxyId.ERC20
+ ? assetDataUtils.decodeERC20AssetData(signedOrderRight.makerAssetData)
+ : assetDataUtils.decodeERC721AssetData(signedOrderRight.makerAssetData);
+ const makerAssetAddressRight = makerERC20AssetDataRight.tokenAddress;
+ if (makerAssetProxyIdLeft === AssetProxyId.ERC20) {
+ expect(
+ realERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft],
+ 'Checking left maker egress ERC20 account balance',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft]);
+ expect(
+ realERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft],
+ 'Checking right maker ingress ERC20 account balance',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft]);
+ expect(
+ realERC20BalancesByOwner[takerAddress][makerAssetAddressLeft],
+ 'Checking taker ingress ERC20 account balance',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[takerAddress][makerAssetAddressLeft]);
+ } else if (makerAssetProxyIdLeft === AssetProxyId.ERC721) {
+ expect(
+ realERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft].sort(),
+ 'Checking left maker egress ERC721 account holdings',
+ ).to.be.deep.equal(
+ expectedERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressLeft].sort(),
+ );
+ expect(
+ realERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft].sort(),
+ 'Checking right maker ERC721 account holdings',
+ ).to.be.deep.equal(
+ expectedERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressLeft].sort(),
+ );
+ expect(
+ realERC721TokenIdsByOwner[takerAddress][makerAssetAddressLeft].sort(),
+ 'Checking taker ingress ERC721 account holdings',
+ ).to.be.deep.equal(expectedERC721TokenIdsByOwner[takerAddress][makerAssetAddressLeft].sort());
+ } else {
+ throw new Error(`Unhandled Asset Proxy ID: ${makerAssetProxyIdLeft}`);
+ }
+ if (makerAssetProxyIdRight === AssetProxyId.ERC20) {
+ expect(
+ realERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight],
+ 'Checking left maker ingress ERC20 account balance',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight]);
+ expect(
+ realERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressRight],
+ 'Checking right maker egress ERC20 account balance',
+ ).to.be.bignumber.equal(
+ expectedERC20BalancesByOwner[signedOrderRight.makerAddress][makerAssetAddressRight],
+ );
+ } else if (makerAssetProxyIdRight === AssetProxyId.ERC721) {
+ expect(
+ realERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight].sort(),
+ 'Checking left maker ingress ERC721 account holdings',
+ ).to.be.deep.equal(
+ expectedERC721TokenIdsByOwner[signedOrderLeft.makerAddress][makerAssetAddressRight].sort(),
+ );
+ expect(
+ realERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressRight],
+ 'Checking right maker agress ERC721 account holdings',
+ ).to.be.deep.equal(expectedERC721TokenIdsByOwner[signedOrderRight.makerAddress][makerAssetAddressRight]);
+ } else {
+ throw new Error(`Unhandled Asset Proxy ID: ${makerAssetProxyIdRight}`);
+ }
+ // Paid fees
+ expect(
+ realERC20BalancesByOwner[signedOrderLeft.makerAddress][this._feeTokenAddress],
+ 'Checking left maker egress ERC20 account fees',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderLeft.makerAddress][this._feeTokenAddress]);
+ expect(
+ realERC20BalancesByOwner[signedOrderRight.makerAddress][this._feeTokenAddress],
+ 'Checking right maker egress ERC20 account fees',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[signedOrderRight.makerAddress][this._feeTokenAddress]);
+ expect(
+ realERC20BalancesByOwner[takerAddress][this._feeTokenAddress],
+ 'Checking taker egress ERC20 account fees',
+ ).to.be.bignumber.equal(expectedERC20BalancesByOwner[takerAddress][this._feeTokenAddress]);
+ // Received fees
+ expect(
+ realERC20BalancesByOwner[signedOrderLeft.feeRecipientAddress][this._feeTokenAddress],
+ 'Checking left fee recipient ingress ERC20 account fees',
+ ).to.be.bignumber.equal(
+ expectedERC20BalancesByOwner[signedOrderLeft.feeRecipientAddress][this._feeTokenAddress],
+ );
+ expect(
+ realERC20BalancesByOwner[signedOrderRight.feeRecipientAddress][this._feeTokenAddress],
+ 'Checking right fee receipient ingress ERC20 account fees',
+ ).to.be.bignumber.equal(
+ expectedERC20BalancesByOwner[signedOrderRight.feeRecipientAddress][this._feeTokenAddress],
+ );
+ }
+} // tslint:disable-line:max-file-line-count
diff --git a/contracts/protocol/test/utils/order_factory_from_scenario.ts b/contracts/protocol/test/utils/order_factory_from_scenario.ts
new file mode 100644
index 000000000..ecb4b2e28
--- /dev/null
+++ b/contracts/protocol/test/utils/order_factory_from_scenario.ts
@@ -0,0 +1,294 @@
+import {
+ AssetDataScenario,
+ constants,
+ ERC721TokenIdsByOwner,
+ ExpirationTimeSecondsScenario,
+ FeeRecipientAddressScenario,
+ OrderAssetAmountScenario,
+ OrderScenario,
+ TakerScenario,
+} from '@0x/contracts-test-utils';
+import { DummyERC721TokenContract } from '@0x/contracts-tokens';
+import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
+import { Order } from '@0x/types';
+import { BigNumber, errorUtils } from '@0x/utils';
+
+const TEN_UNITS_EIGHTEEN_DECIMALS = new BigNumber(10_000_000_000_000_000_000);
+const FIVE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(5_000_000_000_000_000_000);
+const POINT_ONE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(100_000_000_000_000_000);
+const POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(50_000_000_000_000_000);
+const TEN_UNITS_FIVE_DECIMALS = new BigNumber(1_000_000);
+const FIVE_UNITS_FIVE_DECIMALS = new BigNumber(500_000);
+const TEN_UNITS_ZERO_DECIMALS = new BigNumber(10);
+const ONE_THOUSAND_UNITS_ZERO_DECIMALS = new BigNumber(1000);
+const ONE_NFT_UNIT = new BigNumber(1);
+
+export class OrderFactoryFromScenario {
+ private readonly _userAddresses: string[];
+ private readonly _zrxAddress: string;
+ private readonly _nonZrxERC20EighteenDecimalTokenAddresses: string[];
+ private readonly _erc20FiveDecimalTokenAddresses: string[];
+ private readonly _erc20ZeroDecimalTokenAddresses: string[];
+ private readonly _erc721Token: DummyERC721TokenContract;
+ private readonly _erc721Balances: ERC721TokenIdsByOwner;
+ private readonly _exchangeAddress: string;
+ constructor(
+ userAddresses: string[],
+ zrxAddress: string,
+ nonZrxERC20EighteenDecimalTokenAddresses: string[],
+ erc20FiveDecimalTokenAddresses: string[],
+ erc20ZeroDecimalTokenAddresses: string[],
+ erc721Token: DummyERC721TokenContract,
+ erc721Balances: ERC721TokenIdsByOwner,
+ exchangeAddress: string,
+ ) {
+ this._userAddresses = userAddresses;
+ this._zrxAddress = zrxAddress;
+ this._nonZrxERC20EighteenDecimalTokenAddresses = nonZrxERC20EighteenDecimalTokenAddresses;
+ this._erc20FiveDecimalTokenAddresses = erc20FiveDecimalTokenAddresses;
+ this._erc20ZeroDecimalTokenAddresses = erc20ZeroDecimalTokenAddresses;
+ this._erc721Token = erc721Token;
+ this._erc721Balances = erc721Balances;
+ this._exchangeAddress = exchangeAddress;
+ }
+ public generateOrder(orderScenario: OrderScenario): Order {
+ const makerAddress = this._userAddresses[1];
+ let takerAddress = this._userAddresses[2];
+ const erc721MakerAssetIds = this._erc721Balances[makerAddress][this._erc721Token.address];
+ const erc721TakerAssetIds = this._erc721Balances[takerAddress][this._erc721Token.address];
+ let feeRecipientAddress;
+ let makerAssetAmount;
+ let takerAssetAmount;
+ let makerFee;
+ let takerFee;
+ let expirationTimeSeconds;
+ let makerAssetData;
+ let takerAssetData;
+
+ switch (orderScenario.feeRecipientScenario) {
+ case FeeRecipientAddressScenario.BurnAddress:
+ feeRecipientAddress = constants.NULL_ADDRESS;
+ break;
+ case FeeRecipientAddressScenario.EthUserAddress:
+ feeRecipientAddress = this._userAddresses[4];
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('FeeRecipientAddressScenario', orderScenario.feeRecipientScenario);
+ }
+
+ switch (orderScenario.makerAssetDataScenario) {
+ case AssetDataScenario.ZRXFeeToken:
+ makerAssetData = assetDataUtils.encodeERC20AssetData(this._zrxAddress);
+ break;
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ makerAssetData = assetDataUtils.encodeERC20AssetData(this._nonZrxERC20EighteenDecimalTokenAddresses[0]);
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ makerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[0]);
+ break;
+ case AssetDataScenario.ERC721:
+ makerAssetData = assetDataUtils.encodeERC721AssetData(
+ this._erc721Token.address,
+ erc721MakerAssetIds[0],
+ );
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ makerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20ZeroDecimalTokenAddresses[0]);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
+ }
+
+ switch (orderScenario.takerAssetDataScenario) {
+ case AssetDataScenario.ZRXFeeToken:
+ takerAssetData = assetDataUtils.encodeERC20AssetData(this._zrxAddress);
+ break;
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ takerAssetData = assetDataUtils.encodeERC20AssetData(this._nonZrxERC20EighteenDecimalTokenAddresses[1]);
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ takerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20FiveDecimalTokenAddresses[1]);
+ break;
+ case AssetDataScenario.ERC721:
+ takerAssetData = assetDataUtils.encodeERC721AssetData(
+ this._erc721Token.address,
+ erc721TakerAssetIds[0],
+ );
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ takerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20ZeroDecimalTokenAddresses[1]);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
+ }
+
+ switch (orderScenario.makerAssetAmountScenario) {
+ case OrderAssetAmountScenario.Large:
+ switch (orderScenario.makerAssetDataScenario) {
+ case AssetDataScenario.ZRXFeeToken:
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ makerAssetAmount = TEN_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ makerAssetAmount = TEN_UNITS_FIVE_DECIMALS;
+ break;
+ case AssetDataScenario.ERC721:
+ makerAssetAmount = ONE_NFT_UNIT;
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ makerAssetAmount = ONE_THOUSAND_UNITS_ZERO_DECIMALS;
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
+ }
+ break;
+ case OrderAssetAmountScenario.Small:
+ switch (orderScenario.makerAssetDataScenario) {
+ case AssetDataScenario.ZRXFeeToken:
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ makerAssetAmount = FIVE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ makerAssetAmount = FIVE_UNITS_FIVE_DECIMALS;
+ break;
+ case AssetDataScenario.ERC721:
+ makerAssetAmount = ONE_NFT_UNIT;
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ makerAssetAmount = TEN_UNITS_ZERO_DECIMALS;
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario);
+ }
+ break;
+ case OrderAssetAmountScenario.Zero:
+ makerAssetAmount = new BigNumber(0);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.makerAssetAmountScenario);
+ }
+
+ switch (orderScenario.takerAssetAmountScenario) {
+ case OrderAssetAmountScenario.Large:
+ switch (orderScenario.takerAssetDataScenario) {
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ case AssetDataScenario.ZRXFeeToken:
+ takerAssetAmount = TEN_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ takerAssetAmount = TEN_UNITS_FIVE_DECIMALS;
+ break;
+ case AssetDataScenario.ERC721:
+ takerAssetAmount = ONE_NFT_UNIT;
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ takerAssetAmount = ONE_THOUSAND_UNITS_ZERO_DECIMALS;
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
+ }
+ break;
+ case OrderAssetAmountScenario.Small:
+ switch (orderScenario.takerAssetDataScenario) {
+ case AssetDataScenario.ERC20NonZRXEighteenDecimals:
+ case AssetDataScenario.ZRXFeeToken:
+ takerAssetAmount = FIVE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case AssetDataScenario.ERC20FiveDecimals:
+ takerAssetAmount = FIVE_UNITS_FIVE_DECIMALS;
+ break;
+ case AssetDataScenario.ERC721:
+ takerAssetAmount = ONE_NFT_UNIT;
+ break;
+ case AssetDataScenario.ERC20ZeroDecimals:
+ takerAssetAmount = TEN_UNITS_ZERO_DECIMALS;
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario);
+ }
+ break;
+ case OrderAssetAmountScenario.Zero:
+ takerAssetAmount = new BigNumber(0);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.takerAssetAmountScenario);
+ }
+
+ switch (orderScenario.makerFeeScenario) {
+ case OrderAssetAmountScenario.Large:
+ makerFee = POINT_ONE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case OrderAssetAmountScenario.Small:
+ makerFee = POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case OrderAssetAmountScenario.Zero:
+ makerFee = new BigNumber(0);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.makerFeeScenario);
+ }
+
+ switch (orderScenario.takerFeeScenario) {
+ case OrderAssetAmountScenario.Large:
+ takerFee = POINT_ONE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case OrderAssetAmountScenario.Small:
+ takerFee = POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS;
+ break;
+ case OrderAssetAmountScenario.Zero:
+ takerFee = new BigNumber(0);
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr('OrderAssetAmountScenario', orderScenario.takerFeeScenario);
+ }
+
+ switch (orderScenario.expirationTimeSecondsScenario) {
+ case ExpirationTimeSecondsScenario.InFuture:
+ expirationTimeSeconds = new BigNumber(2524604400); // Close to infinite
+ break;
+ case ExpirationTimeSecondsScenario.InPast:
+ expirationTimeSeconds = new BigNumber(0); // Jan 1, 1970
+ break;
+ default:
+ throw errorUtils.spawnSwitchErr(
+ 'ExpirationTimeSecondsScenario',
+ orderScenario.expirationTimeSecondsScenario,
+ );
+ }
+
+ switch (orderScenario.takerScenario) {
+ case TakerScenario.CorrectlySpecified:
+ break; // noop since takerAddress is already specified
+
+ case TakerScenario.IncorrectlySpecified:
+ const notTaker = this._userAddresses[3];
+ takerAddress = notTaker;
+ break;
+
+ case TakerScenario.Unspecified:
+ takerAddress = constants.NULL_ADDRESS;
+ break;
+
+ default:
+ throw errorUtils.spawnSwitchErr('TakerScenario', orderScenario.takerScenario);
+ }
+
+ const order = {
+ senderAddress: constants.NULL_ADDRESS,
+ makerAddress,
+ takerAddress,
+ makerFee,
+ takerFee,
+ makerAssetAmount,
+ takerAssetAmount,
+ makerAssetData,
+ takerAssetData,
+ salt: generatePseudoRandomSalt(),
+ exchangeAddress: this._exchangeAddress,
+ feeRecipientAddress,
+ expirationTimeSeconds,
+ };
+
+ return order;
+ }
+}
diff --git a/contracts/protocol/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts b/contracts/protocol/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts
new file mode 100644
index 000000000..64b7dedbe
--- /dev/null
+++ b/contracts/protocol/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts
@@ -0,0 +1,19 @@
+import { AbstractBalanceAndProxyAllowanceFetcher } from '@0x/order-utils';
+import { BigNumber } from '@0x/utils';
+
+import { AssetWrapper } from './asset_wrapper';
+
+export class SimpleAssetBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher {
+ private readonly _assetWrapper: AssetWrapper;
+ constructor(assetWrapper: AssetWrapper) {
+ this._assetWrapper = assetWrapper;
+ }
+ public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
+ const balance = await this._assetWrapper.getBalanceAsync(userAddress, assetData);
+ return balance;
+ }
+ public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
+ const proxyAllowance = await this._assetWrapper.getProxyAllowanceAsync(userAddress, assetData);
+ return proxyAllowance;
+ }
+}
diff --git a/contracts/protocol/test/utils/simple_order_filled_cancelled_fetcher.ts b/contracts/protocol/test/utils/simple_order_filled_cancelled_fetcher.ts
new file mode 100644
index 000000000..af959e00e
--- /dev/null
+++ b/contracts/protocol/test/utils/simple_order_filled_cancelled_fetcher.ts
@@ -0,0 +1,31 @@
+import { AbstractOrderFilledCancelledFetcher, orderHashUtils } from '@0x/order-utils';
+import { SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+
+import { ExchangeWrapper } from './exchange_wrapper';
+
+export class SimpleOrderFilledCancelledFetcher implements AbstractOrderFilledCancelledFetcher {
+ private readonly _exchangeWrapper: ExchangeWrapper;
+ private readonly _zrxAssetData: string;
+ constructor(exchange: ExchangeWrapper, zrxAssetData: string) {
+ this._exchangeWrapper = exchange;
+ this._zrxAssetData = zrxAssetData;
+ }
+ public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
+ const filledTakerAmount = new BigNumber(await this._exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash));
+ return filledTakerAmount;
+ }
+ public async isOrderCancelledAsync(signedOrder: SignedOrder): Promise<boolean> {
+ const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ const isCancelled = await this._exchangeWrapper.isCancelledAsync(orderHash);
+ const orderEpoch = await this._exchangeWrapper.getOrderEpochAsync(
+ signedOrder.makerAddress,
+ signedOrder.senderAddress,
+ );
+ const isCancelledByOrderEpoch = orderEpoch > signedOrder.salt;
+ return isCancelled || isCancelledByOrderEpoch;
+ }
+ public getZRXAssetData(): string {
+ return this._zrxAssetData;
+ }
+}
diff --git a/contracts/protocol/tsconfig.json b/contracts/protocol/tsconfig.json
new file mode 100644
index 000000000..db872fc32
--- /dev/null
+++ b/contracts/protocol/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../tsconfig",
+ "compilerOptions": {
+ "outDir": "lib",
+ "rootDir": ".",
+ "resolveJsonModule": true
+ },
+ "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
+ "files": [
+ "./generated-artifacts/AssetProxyOwner.json",
+ "./generated-artifacts/ERC20Proxy.json",
+ "./generated-artifacts/ERC721Proxy.json",
+ "./generated-artifacts/Exchange.json",
+ "./generated-artifacts/MixinAuthorizable.json",
+ "./generated-artifacts/MultiAssetProxy.json",
+ "./generated-artifacts/TestAssetProxyDispatcher.json",
+ "./generated-artifacts/TestAssetProxyOwner.json",
+ "./generated-artifacts/TestExchangeInternals.json",
+ "./generated-artifacts/TestSignatureValidator.json",
+ "./generated-artifacts/TestStaticCallReceiver.json"
+ ],
+ "exclude": ["./deploy/solc/solc_bin"]
+}
diff --git a/contracts/protocol/tslint.json b/contracts/protocol/tslint.json
new file mode 100644
index 000000000..1bb3ac2a2
--- /dev/null
+++ b/contracts/protocol/tslint.json
@@ -0,0 +1,6 @@
+{
+ "extends": ["@0x/tslint-config"],
+ "rules": {
+ "custom-no-magic-numbers": false
+ }
+}