From 9f5eeed30930eea59e9922569a15cb5d689f3eeb Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 10 Dec 2018 14:37:48 -0800 Subject: Rename core package to protocol --- .circleci/config.yml | 4 +- .gitignore | 4 +- .prettierignore | 6 +- contracts/core/.solhint.json | 20 - contracts/core/CHANGELOG.json | 121 -- contracts/core/README.md | 74 - contracts/core/compiler.json | 34 - .../contracts/protocol/AssetProxy/ERC20Proxy.sol | 184 --- .../contracts/protocol/AssetProxy/ERC721Proxy.sol | 171 --- .../protocol/AssetProxy/MixinAuthorizable.sol | 117 -- .../protocol/AssetProxy/MultiAssetProxy.sol | 300 ---- .../protocol/AssetProxy/mixins/MAuthorizable.sol | 41 - .../protocol/AssetProxyOwner/AssetProxyOwner.sol | 108 -- .../core/contracts/protocol/Exchange/Exchange.sol | 53 - .../Exchange/MixinAssetProxyDispatcher.sol | 174 --- .../protocol/Exchange/MixinExchangeCore.sol | 529 ------- .../protocol/Exchange/MixinMatchOrders.sol | 335 ----- .../protocol/Exchange/MixinSignatureValidator.sol | 324 ----- .../protocol/Exchange/MixinTransactions.sol | 152 -- .../protocol/Exchange/MixinWrapperFunctions.sol | 426 ------ .../Exchange/mixins/MAssetProxyDispatcher.sol | 45 - .../protocol/Exchange/mixins/MExchangeCore.sol | 157 --- .../protocol/Exchange/mixins/MMatchOrders.sol | 58 - .../Exchange/mixins/MSignatureValidator.sol | 75 - .../protocol/Exchange/mixins/MTransactions.sol | 58 - .../protocol/Exchange/mixins/MWrapperFunctions.sol | 41 - .../TestAssetProxyDispatcher.sol | 37 - .../TestAssetProxyOwner/TestAssetProxyOwner.sol | 58 - .../TestExchangeInternals.sol | 191 --- .../TestSignatureValidator.sol | 45 - .../TestStaticCallReceiver.sol | 81 -- contracts/core/package.json | 96 -- contracts/core/src/artifacts/index.ts | 27 - contracts/core/src/index.ts | 3 - contracts/core/src/wrappers/index.ts | 10 - contracts/core/test/asset_proxy/authorizable.ts | 211 --- contracts/core/test/asset_proxy/proxies.ts | 1253 ----------------- contracts/core/test/exchange/core.ts | 1178 ---------------- contracts/core/test/exchange/dispatcher.ts | 284 ---- contracts/core/test/exchange/fill_order.ts | 314 ----- contracts/core/test/exchange/internal.ts | 472 ------- contracts/core/test/exchange/match_orders.ts | 1284 ----------------- .../core/test/exchange/signature_validator.ts | 526 ------- contracts/core/test/exchange/transactions.ts | 466 ------- contracts/core/test/exchange/wrapper.ts | 1461 -------------------- contracts/core/test/global_hooks.ts | 17 - contracts/core/test/multisig/asset_proxy_owner.ts | 506 ------- .../core/test/utils/asset_proxy_owner_wrapper.ts | 70 - contracts/core/test/utils/asset_wrapper.ts | 222 --- contracts/core/test/utils/erc20_wrapper.ts | 179 --- contracts/core/test/utils/erc721_wrapper.ts | 236 ---- contracts/core/test/utils/exchange_wrapper.ts | 281 ---- .../test/utils/fill_order_combinatorial_utils.ts | 928 ------------- contracts/core/test/utils/index.ts | 3 - contracts/core/test/utils/match_order_tester.ts | 562 -------- .../core/test/utils/order_factory_from_scenario.ts | 294 ---- ...le_asset_balance_and_proxy_allowance_fetcher.ts | 19 - .../utils/simple_order_filled_cancelled_fetcher.ts | 31 - contracts/core/tsconfig.json | 23 - contracts/core/tslint.json | 6 - contracts/examples/package.json | 2 +- contracts/extensions/package.json | 4 +- .../extensions/test/extensions/dutch_auction.ts | 6 +- contracts/extensions/test/extensions/forwarder.ts | 8 +- .../extensions/test/extensions/order_validator.ts | 6 +- .../extensions/test/utils/forwarder_wrapper.ts | 8 +- contracts/multisig/package.json | 2 +- contracts/protocol/.solhint.json | 20 + contracts/protocol/CHANGELOG.json | 121 ++ contracts/protocol/README.md | 74 + contracts/protocol/compiler.json | 34 + .../contracts/protocol/AssetProxy/ERC20Proxy.sol | 184 +++ .../contracts/protocol/AssetProxy/ERC721Proxy.sol | 171 +++ .../protocol/AssetProxy/MixinAuthorizable.sol | 117 ++ .../protocol/AssetProxy/MultiAssetProxy.sol | 300 ++++ .../protocol/AssetProxy/mixins/MAuthorizable.sol | 41 + .../protocol/AssetProxyOwner/AssetProxyOwner.sol | 108 ++ .../contracts/protocol/Exchange/Exchange.sol | 53 + .../Exchange/MixinAssetProxyDispatcher.sol | 174 +++ .../protocol/Exchange/MixinExchangeCore.sol | 529 +++++++ .../protocol/Exchange/MixinMatchOrders.sol | 335 +++++ .../protocol/Exchange/MixinSignatureValidator.sol | 324 +++++ .../protocol/Exchange/MixinTransactions.sol | 152 ++ .../protocol/Exchange/MixinWrapperFunctions.sol | 426 ++++++ .../Exchange/mixins/MAssetProxyDispatcher.sol | 45 + .../protocol/Exchange/mixins/MExchangeCore.sol | 157 +++ .../protocol/Exchange/mixins/MMatchOrders.sol | 58 + .../Exchange/mixins/MSignatureValidator.sol | 75 + .../protocol/Exchange/mixins/MTransactions.sol | 58 + .../protocol/Exchange/mixins/MWrapperFunctions.sol | 41 + .../TestAssetProxyDispatcher.sol | 37 + .../TestAssetProxyOwner/TestAssetProxyOwner.sol | 58 + .../TestExchangeInternals.sol | 191 +++ .../TestSignatureValidator.sol | 45 + .../TestStaticCallReceiver.sol | 81 ++ contracts/protocol/package.json | 96 ++ contracts/protocol/src/artifacts/index.ts | 27 + contracts/protocol/src/index.ts | 3 + contracts/protocol/src/wrappers/index.ts | 10 + .../protocol/test/asset_proxy/authorizable.ts | 211 +++ contracts/protocol/test/asset_proxy/proxies.ts | 1253 +++++++++++++++++ contracts/protocol/test/exchange/core.ts | 1178 ++++++++++++++++ contracts/protocol/test/exchange/dispatcher.ts | 284 ++++ contracts/protocol/test/exchange/fill_order.ts | 314 +++++ contracts/protocol/test/exchange/internal.ts | 472 +++++++ contracts/protocol/test/exchange/match_orders.ts | 1284 +++++++++++++++++ .../protocol/test/exchange/signature_validator.ts | 526 +++++++ contracts/protocol/test/exchange/transactions.ts | 466 +++++++ contracts/protocol/test/exchange/wrapper.ts | 1461 ++++++++++++++++++++ contracts/protocol/test/global_hooks.ts | 17 + .../protocol/test/multisig/asset_proxy_owner.ts | 506 +++++++ .../test/utils/asset_proxy_owner_wrapper.ts | 70 + contracts/protocol/test/utils/asset_wrapper.ts | 222 +++ contracts/protocol/test/utils/erc20_wrapper.ts | 179 +++ contracts/protocol/test/utils/erc721_wrapper.ts | 236 ++++ contracts/protocol/test/utils/exchange_wrapper.ts | 281 ++++ .../test/utils/fill_order_combinatorial_utils.ts | 928 +++++++++++++ contracts/protocol/test/utils/index.ts | 3 + .../protocol/test/utils/match_order_tester.ts | 562 ++++++++ .../test/utils/order_factory_from_scenario.ts | 294 ++++ ...le_asset_balance_and_proxy_allowance_fetcher.ts | 19 + .../utils/simple_order_filled_cancelled_fetcher.ts | 31 + contracts/protocol/tsconfig.json | 23 + contracts/protocol/tslint.json | 6 + 124 files changed, 14999 insertions(+), 14993 deletions(-) delete mode 100644 contracts/core/.solhint.json delete mode 100644 contracts/core/CHANGELOG.json delete mode 100644 contracts/core/README.md delete mode 100644 contracts/core/compiler.json delete mode 100644 contracts/core/contracts/protocol/AssetProxy/ERC20Proxy.sol delete mode 100644 contracts/core/contracts/protocol/AssetProxy/ERC721Proxy.sol delete mode 100644 contracts/core/contracts/protocol/AssetProxy/MixinAuthorizable.sol delete mode 100644 contracts/core/contracts/protocol/AssetProxy/MultiAssetProxy.sol delete mode 100644 contracts/core/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol delete mode 100644 contracts/core/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/Exchange.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/MixinExchangeCore.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/MixinMatchOrders.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/MixinSignatureValidator.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/MixinTransactions.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/MixinWrapperFunctions.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/mixins/MExchangeCore.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/mixins/MMatchOrders.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/mixins/MSignatureValidator.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/mixins/MTransactions.sol delete mode 100644 contracts/core/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol delete mode 100644 contracts/core/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol delete mode 100644 contracts/core/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol delete mode 100644 contracts/core/contracts/test/TestExchangeInternals/TestExchangeInternals.sol delete mode 100644 contracts/core/contracts/test/TestSignatureValidator/TestSignatureValidator.sol delete mode 100644 contracts/core/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol delete mode 100644 contracts/core/package.json delete mode 100644 contracts/core/src/artifacts/index.ts delete mode 100644 contracts/core/src/index.ts delete mode 100644 contracts/core/src/wrappers/index.ts delete mode 100644 contracts/core/test/asset_proxy/authorizable.ts delete mode 100644 contracts/core/test/asset_proxy/proxies.ts delete mode 100644 contracts/core/test/exchange/core.ts delete mode 100644 contracts/core/test/exchange/dispatcher.ts delete mode 100644 contracts/core/test/exchange/fill_order.ts delete mode 100644 contracts/core/test/exchange/internal.ts delete mode 100644 contracts/core/test/exchange/match_orders.ts delete mode 100644 contracts/core/test/exchange/signature_validator.ts delete mode 100644 contracts/core/test/exchange/transactions.ts delete mode 100644 contracts/core/test/exchange/wrapper.ts delete mode 100644 contracts/core/test/global_hooks.ts delete mode 100644 contracts/core/test/multisig/asset_proxy_owner.ts delete mode 100644 contracts/core/test/utils/asset_proxy_owner_wrapper.ts delete mode 100644 contracts/core/test/utils/asset_wrapper.ts delete mode 100644 contracts/core/test/utils/erc20_wrapper.ts delete mode 100644 contracts/core/test/utils/erc721_wrapper.ts delete mode 100644 contracts/core/test/utils/exchange_wrapper.ts delete mode 100644 contracts/core/test/utils/fill_order_combinatorial_utils.ts delete mode 100644 contracts/core/test/utils/index.ts delete mode 100644 contracts/core/test/utils/match_order_tester.ts delete mode 100644 contracts/core/test/utils/order_factory_from_scenario.ts delete mode 100644 contracts/core/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts delete mode 100644 contracts/core/test/utils/simple_order_filled_cancelled_fetcher.ts delete mode 100644 contracts/core/tsconfig.json delete mode 100644 contracts/core/tslint.json create mode 100644 contracts/protocol/.solhint.json create mode 100644 contracts/protocol/CHANGELOG.json create mode 100644 contracts/protocol/README.md create mode 100644 contracts/protocol/compiler.json create mode 100644 contracts/protocol/contracts/protocol/AssetProxy/ERC20Proxy.sol create mode 100644 contracts/protocol/contracts/protocol/AssetProxy/ERC721Proxy.sol create mode 100644 contracts/protocol/contracts/protocol/AssetProxy/MixinAuthorizable.sol create mode 100644 contracts/protocol/contracts/protocol/AssetProxy/MultiAssetProxy.sol create mode 100644 contracts/protocol/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol create mode 100644 contracts/protocol/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/Exchange.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/MixinExchangeCore.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/MixinMatchOrders.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/MixinSignatureValidator.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/MixinTransactions.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/MixinWrapperFunctions.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/mixins/MExchangeCore.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/mixins/MMatchOrders.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/mixins/MSignatureValidator.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/mixins/MTransactions.sol create mode 100644 contracts/protocol/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol create mode 100644 contracts/protocol/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol create mode 100644 contracts/protocol/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol create mode 100644 contracts/protocol/contracts/test/TestExchangeInternals/TestExchangeInternals.sol create mode 100644 contracts/protocol/contracts/test/TestSignatureValidator/TestSignatureValidator.sol create mode 100644 contracts/protocol/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol create mode 100644 contracts/protocol/package.json create mode 100644 contracts/protocol/src/artifacts/index.ts create mode 100644 contracts/protocol/src/index.ts create mode 100644 contracts/protocol/src/wrappers/index.ts create mode 100644 contracts/protocol/test/asset_proxy/authorizable.ts create mode 100644 contracts/protocol/test/asset_proxy/proxies.ts create mode 100644 contracts/protocol/test/exchange/core.ts create mode 100644 contracts/protocol/test/exchange/dispatcher.ts create mode 100644 contracts/protocol/test/exchange/fill_order.ts create mode 100644 contracts/protocol/test/exchange/internal.ts create mode 100644 contracts/protocol/test/exchange/match_orders.ts create mode 100644 contracts/protocol/test/exchange/signature_validator.ts create mode 100644 contracts/protocol/test/exchange/transactions.ts create mode 100644 contracts/protocol/test/exchange/wrapper.ts create mode 100644 contracts/protocol/test/global_hooks.ts create mode 100644 contracts/protocol/test/multisig/asset_proxy_owner.ts create mode 100644 contracts/protocol/test/utils/asset_proxy_owner_wrapper.ts create mode 100644 contracts/protocol/test/utils/asset_wrapper.ts create mode 100644 contracts/protocol/test/utils/erc20_wrapper.ts create mode 100644 contracts/protocol/test/utils/erc721_wrapper.ts create mode 100644 contracts/protocol/test/utils/exchange_wrapper.ts create mode 100644 contracts/protocol/test/utils/fill_order_combinatorial_utils.ts create mode 100644 contracts/protocol/test/utils/index.ts create mode 100644 contracts/protocol/test/utils/match_order_tester.ts create mode 100644 contracts/protocol/test/utils/order_factory_from_scenario.ts create mode 100644 contracts/protocol/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts create mode 100644 contracts/protocol/test/utils/simple_order_filled_cancelled_fetcher.ts create mode 100644 contracts/protocol/tsconfig.json create mode 100644 contracts/protocol/tslint.json diff --git a/.circleci/config.yml b/.circleci/config.yml index b7f68ed29..de620d83b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,7 +45,7 @@ jobs: - run: yarn wsrun test:circleci @0x/contracts-libs - run: yarn wsrun test:circleci @0x/contracts-tokens - run: yarn wsrun test:circleci @0x/contracts-extensions - - run: yarn wsrun test:circleci @0x/contracts-core + - run: yarn wsrun test:circleci @0x/contracts-protocol test-contracts-geth: docker: - image: circleci/node:9 @@ -62,7 +62,7 @@ jobs: - run: TEST_PROVIDER=geth yarn wsrun test @0x/contracts-libs - run: TEST_PROVIDER=geth yarn wsrun test @0x/contracts-tokens - run: TEST_PROVIDER=geth yarn wsrun test @0x/contracts-extensions - - run: TEST_PROVIDER=geth yarn wsrun test @0x/contracts-core + - run: TEST_PROVIDER=geth yarn wsrun test @0x/contracts-protocol test-publish: resource_class: medium+ docker: diff --git a/.gitignore b/.gitignore index ee38ec5dd..2265ab9df 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,7 @@ packages/react-docs/example/public/bundle* packages/testnet-faucets/server/ # generated contract artifacts/ -contracts/core/generated-artifacts/ +contracts/protocol/generated-artifacts/ contracts/multisig/generated-artifacts/ contracts/utils/generated-artifacts/ contracts/libs/generated-artifacts/ @@ -92,7 +92,7 @@ packages/metacoin/artifacts/ # generated contract wrappers packages/abi-gen-wrappers/wrappers -contracts/core/generated-wrappers/ +contracts/protocol/generated-wrappers/ contracts/multisig/generated-wrappers/ contracts/utils/generated-wrappers/ contracts/libs/generated-wrappers/ diff --git a/.prettierignore b/.prettierignore index d50245827..7f8662b0a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,7 @@ lib .nyc_output -/contracts/core/generated-wrappers -/contracts/core/generated-artifacts +/contracts/protocol/generated-wrappers +/contracts/protocol/generated-artifacts /contracts/multisig/generated-wrappers /contracts/multisig/generated-artifacts /contracts/utils/generated-wrappers @@ -14,6 +14,8 @@ lib /contracts/tokens/generated-artifacts /contracts/examples/generated-wrappers /contracts/examples/generated-artifacts +/contracts/extensions/generated-wrappers +/contracts/extensions/generated-artifacts /packages/abi-gen-wrappers/src/generated-wrappers /packages/contract-artifacts/artifacts /python-packages/order_utils/src/zero_ex/contract_artifacts/artifacts diff --git a/contracts/core/.solhint.json b/contracts/core/.solhint.json deleted file mode 100644 index 076afe9f3..000000000 --- a/contracts/core/.solhint.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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/core/CHANGELOG.json b/contracts/core/CHANGELOG.json deleted file mode 100644 index 371f18cd4..000000000 --- a/contracts/core/CHANGELOG.json +++ /dev/null @@ -1,121 +0,0 @@ -[ - { - "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/core/README.md b/contracts/core/README.md deleted file mode 100644 index d055705c2..000000000 --- a/contracts/core/README.md +++ /dev/null @@ -1,74 +0,0 @@ -## 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-core yarn build -``` - -Or continuously rebuild on change: - -```bash -PKG=@0x/contracts-core 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/core/compiler.json b/contracts/core/compiler.json deleted file mode 100644 index 10e5bb0a1..000000000 --- a/contracts/core/compiler.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "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/core/contracts/protocol/AssetProxy/ERC20Proxy.sol b/contracts/core/contracts/protocol/AssetProxy/ERC20Proxy.sol deleted file mode 100644 index 258443bca..000000000 --- a/contracts/core/contracts/protocol/AssetProxy/ERC20Proxy.sol +++ /dev/null @@ -1,184 +0,0 @@ -/* - - 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/core/contracts/protocol/AssetProxy/ERC721Proxy.sol b/contracts/core/contracts/protocol/AssetProxy/ERC721Proxy.sol deleted file mode 100644 index 65b664b8b..000000000 --- a/contracts/core/contracts/protocol/AssetProxy/ERC721Proxy.sol +++ /dev/null @@ -1,171 +0,0 @@ -/* - - 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/core/contracts/protocol/AssetProxy/MixinAuthorizable.sol b/contracts/core/contracts/protocol/AssetProxy/MixinAuthorizable.sol deleted file mode 100644 index 08f9b94dc..000000000 --- a/contracts/core/contracts/protocol/AssetProxy/MixinAuthorizable.sol +++ /dev/null @@ -1,117 +0,0 @@ -/* - - 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/core/contracts/protocol/AssetProxy/MultiAssetProxy.sol b/contracts/core/contracts/protocol/AssetProxy/MultiAssetProxy.sol deleted file mode 100644 index 42231e73b..000000000 --- a/contracts/core/contracts/protocol/AssetProxy/MultiAssetProxy.sol +++ /dev/null @@ -1,300 +0,0 @@ -/* - - 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/core/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol b/contracts/core/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol deleted file mode 100644 index fe77048ce..000000000 --- a/contracts/core/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol +++ /dev/null @@ -1,41 +0,0 @@ -/* - - 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/core/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol b/contracts/core/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol deleted file mode 100644 index bfc7b5a66..000000000 --- a/contracts/core/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol +++ /dev/null @@ -1,108 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/Exchange.sol b/contracts/core/contracts/protocol/Exchange/Exchange.sol deleted file mode 100644 index 65ca742ea..000000000 --- a/contracts/core/contracts/protocol/Exchange/Exchange.sol +++ /dev/null @@ -1,53 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol b/contracts/core/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol deleted file mode 100644 index 36ab39b45..000000000 --- a/contracts/core/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol +++ /dev/null @@ -1,174 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/MixinExchangeCore.sol b/contracts/core/contracts/protocol/Exchange/MixinExchangeCore.sol deleted file mode 100644 index 68d6a3897..000000000 --- a/contracts/core/contracts/protocol/Exchange/MixinExchangeCore.sol +++ /dev/null @@ -1,529 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/MixinMatchOrders.sol b/contracts/core/contracts/protocol/Exchange/MixinMatchOrders.sol deleted file mode 100644 index fc6d73482..000000000 --- a/contracts/core/contracts/protocol/Exchange/MixinMatchOrders.sol +++ /dev/null @@ -1,335 +0,0 @@ -/* - 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: - // / >= / - // AND - // / >= / - // 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/core/contracts/protocol/Exchange/MixinSignatureValidator.sol b/contracts/core/contracts/protocol/Exchange/MixinSignatureValidator.sol deleted file mode 100644 index 80b4c0755..000000000 --- a/contracts/core/contracts/protocol/Exchange/MixinSignatureValidator.sol +++ /dev/null @@ -1,324 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/MixinTransactions.sol b/contracts/core/contracts/protocol/Exchange/MixinTransactions.sol deleted file mode 100644 index 87c614382..000000000 --- a/contracts/core/contracts/protocol/Exchange/MixinTransactions.sol +++ /dev/null @@ -1,152 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/MixinWrapperFunctions.sol b/contracts/core/contracts/protocol/Exchange/MixinWrapperFunctions.sol deleted file mode 100644 index 2d43432ff..000000000 --- a/contracts/core/contracts/protocol/Exchange/MixinWrapperFunctions.sol +++ /dev/null @@ -1,426 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol b/contracts/core/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol deleted file mode 100644 index fe3d03326..000000000 --- a/contracts/core/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol +++ /dev/null @@ -1,45 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/mixins/MExchangeCore.sol b/contracts/core/contracts/protocol/Exchange/mixins/MExchangeCore.sol deleted file mode 100644 index 215284900..000000000 --- a/contracts/core/contracts/protocol/Exchange/mixins/MExchangeCore.sol +++ /dev/null @@ -1,157 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/mixins/MMatchOrders.sol b/contracts/core/contracts/protocol/Exchange/mixins/MMatchOrders.sol deleted file mode 100644 index 1eb4be329..000000000 --- a/contracts/core/contracts/protocol/Exchange/mixins/MMatchOrders.sol +++ /dev/null @@ -1,58 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/mixins/MSignatureValidator.sol b/contracts/core/contracts/protocol/Exchange/mixins/MSignatureValidator.sol deleted file mode 100644 index a933976d1..000000000 --- a/contracts/core/contracts/protocol/Exchange/mixins/MSignatureValidator.sol +++ /dev/null @@ -1,75 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/mixins/MTransactions.sol b/contracts/core/contracts/protocol/Exchange/mixins/MTransactions.sol deleted file mode 100644 index a6b0fdc85..000000000 --- a/contracts/core/contracts/protocol/Exchange/mixins/MTransactions.sol +++ /dev/null @@ -1,58 +0,0 @@ -/* - - 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/core/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol b/contracts/core/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol deleted file mode 100644 index 101e7cb82..000000000 --- a/contracts/core/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol +++ /dev/null @@ -1,41 +0,0 @@ -/* - - 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/core/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol b/contracts/core/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol deleted file mode 100644 index ad71fc9a1..000000000 --- a/contracts/core/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol +++ /dev/null @@ -1,37 +0,0 @@ -/* - - 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/core/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol b/contracts/core/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol deleted file mode 100644 index 52c66cb56..000000000 --- a/contracts/core/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol +++ /dev/null @@ -1,58 +0,0 @@ -/* - - 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/core/contracts/test/TestExchangeInternals/TestExchangeInternals.sol b/contracts/core/contracts/test/TestExchangeInternals/TestExchangeInternals.sol deleted file mode 100644 index 27187f8f8..000000000 --- a/contracts/core/contracts/test/TestExchangeInternals/TestExchangeInternals.sol +++ /dev/null @@ -1,191 +0,0 @@ -/* - - 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/core/contracts/test/TestSignatureValidator/TestSignatureValidator.sol b/contracts/core/contracts/test/TestSignatureValidator/TestSignatureValidator.sol deleted file mode 100644 index ea3e2de59..000000000 --- a/contracts/core/contracts/test/TestSignatureValidator/TestSignatureValidator.sol +++ /dev/null @@ -1,45 +0,0 @@ -/* - - 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/core/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol b/contracts/core/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol deleted file mode 100644 index d08da7303..000000000 --- a/contracts/core/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol +++ /dev/null @@ -1,81 +0,0 @@ -/* - - 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/core/package.json b/contracts/core/package.json deleted file mode 100644 index b7077f4b4..000000000 --- a/contracts/core/package.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "private": true, - "name": "@0x/contracts-core", - "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/core/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/core/src/artifacts/index.ts b/contracts/core/src/artifacts/index.ts deleted file mode 100644 index c5d12f10b..000000000 --- a/contracts/core/src/artifacts/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -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/core/src/index.ts b/contracts/core/src/index.ts deleted file mode 100644 index ba813e7ca..000000000 --- a/contracts/core/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './artifacts'; -export * from './wrappers'; -export * from '../test/utils'; diff --git a/contracts/core/src/wrappers/index.ts b/contracts/core/src/wrappers/index.ts deleted file mode 100644 index 01b121054..000000000 --- a/contracts/core/src/wrappers/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -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/core/test/asset_proxy/authorizable.ts b/contracts/core/test/asset_proxy/authorizable.ts deleted file mode 100644 index 853d18be0..000000000 --- a/contracts/core/test/asset_proxy/authorizable.ts +++ /dev/null @@ -1,211 +0,0 @@ -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/core/test/asset_proxy/proxies.ts b/contracts/core/test/asset_proxy/proxies.ts deleted file mode 100644 index c4bd95905..000000000 --- a/contracts/core/test/asset_proxy/proxies.ts +++ /dev/null @@ -1,1253 +0,0 @@ -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/core/test/exchange/core.ts b/contracts/core/test/exchange/core.ts deleted file mode 100644 index 9b948f991..000000000 --- a/contracts/core/test/exchange/core.ts +++ /dev/null @@ -1,1178 +0,0 @@ -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).event === 'Transfer', - ); - expect(transferLogs.length).to.be.equal(2); - expect((transferLogs[0] as LogWithDecodedArgs).address).to.be.equal( - zrxToken.address, - ); - expect((transferLogs[0] as LogWithDecodedArgs).args._from).to.be.equal( - makerAddress, - ); - expect((transferLogs[0] as LogWithDecodedArgs).args._to).to.be.equal( - feeRecipientAddress, - ); - expect( - (transferLogs[0] as LogWithDecodedArgs).args._value, - ).to.be.bignumber.equal(signedOrder.makerFee); - expect((transferLogs[1] as LogWithDecodedArgs).address).to.be.equal( - zrxToken.address, - ); - expect((transferLogs[1] as LogWithDecodedArgs).args._from).to.be.equal( - makerAddress, - ); - expect((transferLogs[1] as LogWithDecodedArgs).args._to).to.be.equal( - feeRecipientAddress, - ); - expect( - (transferLogs[1] as LogWithDecodedArgs).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; - 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/core/test/exchange/dispatcher.ts b/contracts/core/test/exchange/dispatcher.ts deleted file mode 100644 index 5b1ac1e20..000000000 --- a/contracts/core/test/exchange/dispatcher.ts +++ /dev/null @@ -1,284 +0,0 @@ -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; - 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/core/test/exchange/fill_order.ts b/contracts/core/test/exchange/fill_order.ts deleted file mode 100644 index 2bdbe4855..000000000 --- a/contracts/core/test/exchange/fill_order.ts +++ /dev/null @@ -1,314 +0,0 @@ -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/core/test/exchange/internal.ts b/contracts/core/test/exchange/internal.ts deleted file mode 100644 index 972f5efb6..000000000 --- a/contracts/core/test/exchange/internal.ts +++ /dev/null @@ -1,472 +0,0 @@ -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 { - 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 { - 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 { - 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 { - // 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 { - 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 { - // 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - const totalFilledAmount = takerAssetFilledAmount.add(orderTakerAssetFilledAmount); - if (totalFilledAmount.greaterThan(MAX_UINT256)) { - throw overflowErrorForSendTransaction; - } - return totalFilledAmount; - } - async function testUpdateFilledStateAsync( - takerAssetFilledAmount: BigNumber, - orderTakerAssetFilledAmount: BigNumber, - orderHash: string, - ): Promise { - 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/core/test/exchange/match_orders.ts b/contracts/core/test/exchange/match_orders.ts deleted file mode 100644 index 8e943c237..000000000 --- a/contracts/core/test/exchange/match_orders.ts +++ /dev/null @@ -1,1284 +0,0 @@ -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/core/test/exchange/signature_validator.ts b/contracts/core/test/exchange/signature_validator.ts deleted file mode 100644 index 62579cdc7..000000000 --- a/contracts/core/test/exchange/signature_validator.ts +++ /dev/null @@ -1,526 +0,0 @@ -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; - 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; - 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/core/test/exchange/transactions.ts b/contracts/core/test/exchange/transactions.ts deleted file mode 100644 index 746f3cb04..000000000 --- a/contracts/core/test/exchange/transactions.ts +++ /dev/null @@ -1,466 +0,0 @@ -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/core/test/exchange/wrapper.ts b/contracts/core/test/exchange/wrapper.ts deleted file mode 100644 index c9efb0c9d..000000000 --- a/contracts/core/test/exchange/wrapper.ts +++ /dev/null @@ -1,1461 +0,0 @@ -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/core/test/global_hooks.ts b/contracts/core/test/global_hooks.ts deleted file mode 100644 index f8ace376a..000000000 --- a/contracts/core/test/global_hooks.ts +++ /dev/null @@ -1,17 +0,0 @@ -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/core/test/multisig/asset_proxy_owner.ts b/contracts/core/test/multisig/asset_proxy_owner.ts deleted file mode 100644 index daebfb7fb..000000000 --- a/contracts/core/test/multisig/asset_proxy_owner.ts +++ /dev/null @@ -1,506 +0,0 @@ -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; - 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; - 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; - 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; - - 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; - 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; - 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; - 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; - 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; - 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; - 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; - 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; - 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; - 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; - 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; - 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; - 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/core/test/utils/asset_proxy_owner_wrapper.ts b/contracts/core/test/utils/asset_proxy_owner_wrapper.ts deleted file mode 100644 index df23658d8..000000000 --- a/contracts/core/test/utils/asset_proxy_owner_wrapper.ts +++ /dev/null @@ -1,70 +0,0 @@ -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 { - 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 { - 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 { - 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 { - 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 { - // 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/core/test/utils/asset_wrapper.ts b/contracts/core/test/utils/asset_wrapper.ts deleted file mode 100644 index e4090ad74..000000000 --- a/contracts/core/test/utils/asset_wrapper.ts +++ /dev/null @@ -1,222 +0,0 @@ -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 { - 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 { - 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 { - 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 { - 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/core/test/utils/erc20_wrapper.ts b/contracts/core/test/utils/erc20_wrapper.ts deleted file mode 100644 index e5ab0e36c..000000000 --- a/contracts/core/test/utils/erc20_wrapper.ts +++ /dev/null @@ -1,179 +0,0 @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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/core/test/utils/erc721_wrapper.ts b/contracts/core/test/utils/erc721_wrapper.ts deleted file mode 100644 index 1f658ce86..000000000 --- a/contracts/core/test/utils/erc721_wrapper.ts +++ /dev/null @@ -1,236 +0,0 @@ -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 { - // 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 { - 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 { - 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 { - 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 { - const proxyAddress = (this._proxyContract as ERC721ProxyContract).address; - await this.approveAsync(proxyAddress, tokenAddress, tokenId); - } - public async approveProxyForAllAsync(tokenAddress: string, tokenId: BigNumber, isApproved: boolean): Promise { - 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 { - 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 { - 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 { - 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 { - 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 { - const tokenContract = this._getTokenContractFromAssetData(tokenAddress); - const owner = await tokenContract.ownerOf.callAsync(tokenId); - return owner; - } - public async isOwnerAsync(userAddress: string, tokenAddress: string, tokenId: BigNumber): Promise { - 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 { - 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 { - 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 { - 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/core/test/utils/exchange_wrapper.ts b/contracts/core/test/utils/exchange_wrapper.ts deleted file mode 100644 index 075d2cb96..000000000 --- a/contracts/core/test/utils/exchange_wrapper.ts +++ /dev/null @@ -1,281 +0,0 @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - const filledAmount = await this._exchange.filled.callAsync(orderHashHex); - return filledAmount; - } - public async isCancelledAsync(orderHashHex: string): Promise { - const isCancelled = await this._exchange.cancelled.callAsync(orderHashHex); - return isCancelled; - } - public async getOrderEpochAsync(makerAddress: string, senderAddress: string): Promise { - const orderEpoch = await this._exchange.orderEpoch.callAsync(makerAddress, senderAddress); - return orderEpoch; - } - public async getOrderInfoAsync(signedOrder: SignedOrder): Promise { - const orderInfo = (await this._exchange.getOrderInfo.callAsync(signedOrder)) as OrderInfo; - return orderInfo; - } - public async getOrdersInfoAsync(signedOrders: SignedOrder[]): Promise { - const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[]; - return ordersInfo; - } - public async matchOrdersAsync( - signedOrderLeft: SignedOrder, - signedOrderRight: SignedOrder, - from: string, - ): Promise { - 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 { - 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/core/test/utils/fill_order_combinatorial_utils.ts b/contracts/core/test/utils/fill_order_combinatorial_utils.ts deleted file mode 100644 index 5d0ea07a8..000000000 --- a/contracts/core/test/utils/fill_order_combinatorial_utils.ts +++ /dev/null @@ -1,928 +0,0 @@ -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, -): Promise { - 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 { - // 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 { - 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; - 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 { - 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 { - 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 { - 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/core/test/utils/index.ts b/contracts/core/test/utils/index.ts deleted file mode 100644 index 75cd88666..000000000 --- a/contracts/core/test/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './exchange_wrapper'; -export * from './erc20_wrapper'; -export * from './erc721_wrapper'; diff --git a/contracts/core/test/utils/match_order_tester.ts b/contracts/core/test/utils/match_order_tester.ts deleted file mode 100644 index 8f574704e..000000000 --- a/contracts/core/test/utils/match_order_tester.ts +++ /dev/null @@ -1,562 +0,0 @@ -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 { - // 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 { - // 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 { - // 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 { - // 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 { - 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 { - // 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/core/test/utils/order_factory_from_scenario.ts b/contracts/core/test/utils/order_factory_from_scenario.ts deleted file mode 100644 index ecb4b2e28..000000000 --- a/contracts/core/test/utils/order_factory_from_scenario.ts +++ /dev/null @@ -1,294 +0,0 @@ -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/core/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts b/contracts/core/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts deleted file mode 100644 index 64b7dedbe..000000000 --- a/contracts/core/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 { - const balance = await this._assetWrapper.getBalanceAsync(userAddress, assetData); - return balance; - } - public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise { - const proxyAllowance = await this._assetWrapper.getProxyAllowanceAsync(userAddress, assetData); - return proxyAllowance; - } -} diff --git a/contracts/core/test/utils/simple_order_filled_cancelled_fetcher.ts b/contracts/core/test/utils/simple_order_filled_cancelled_fetcher.ts deleted file mode 100644 index af959e00e..000000000 --- a/contracts/core/test/utils/simple_order_filled_cancelled_fetcher.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 { - const filledTakerAmount = new BigNumber(await this._exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash)); - return filledTakerAmount; - } - public async isOrderCancelledAsync(signedOrder: SignedOrder): Promise { - 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/core/tsconfig.json b/contracts/core/tsconfig.json deleted file mode 100644 index db872fc32..000000000 --- a/contracts/core/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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/core/tslint.json b/contracts/core/tslint.json deleted file mode 100644 index 1bb3ac2a2..000000000 --- a/contracts/core/tslint.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": ["@0x/tslint-config"], - "rules": { - "custom-no-magic-numbers": false - } -} diff --git a/contracts/examples/package.json b/contracts/examples/package.json index 7682e0d45..5413f9e61 100644 --- a/contracts/examples/package.json +++ b/contracts/examples/package.json @@ -31,7 +31,7 @@ "bugs": { "url": "https://github.com/0xProject/0x-monorepo/issues" }, - "homepage": "https://github.com/0xProject/0x-monorepo/contracts/core/README.md", + "homepage": "https://github.com/0xProject/0x-monorepo/contracts/examples/README.md", "devDependencies": { "@0x/contracts-test-utils": "^1.0.0", "@0x/abi-gen": "^1.0.17", diff --git a/contracts/extensions/package.json b/contracts/extensions/package.json index 95db0c5ac..45838c825 100644 --- a/contracts/extensions/package.json +++ b/contracts/extensions/package.json @@ -43,7 +43,7 @@ "bugs": { "url": "https://github.com/0xProject/0x-monorepo/issues" }, - "homepage": "https://github.com/0xProject/0x-monorepo/contracts/core/README.md", + "homepage": "https://github.com/0xProject/0x-monorepo/contracts/extensions/README.md", "devDependencies": { "@0x/contracts-test-utils": "^1.0.0", "@0x/abi-gen": "^1.0.17", @@ -75,7 +75,7 @@ "@0x/base-contract": "^3.0.8", "@0x/order-utils": "^3.0.4", "@0x/contracts-utils": "^1.0.0", - "@0x/contracts-core": "^2.1.56", + "@0x/contracts-protocol": "^2.1.56", "@0x/contracts-tokens": "^1.0.0", "@0x/contracts-libs": "^1.0.0", "@0x/contracts-interfaces": "^1.0.0", diff --git a/contracts/extensions/test/extensions/dutch_auction.ts b/contracts/extensions/test/extensions/dutch_auction.ts index 2db3be752..6c3b2f0f3 100644 --- a/contracts/extensions/test/extensions/dutch_auction.ts +++ b/contracts/extensions/test/extensions/dutch_auction.ts @@ -1,10 +1,10 @@ import { - artifacts as coreArtifacts, + artifacts as protocolArtifacts, ERC20Wrapper, ERC721Wrapper, ExchangeContract, ExchangeWrapper, -} from '@0x/contracts-core'; +} from '@0x/contracts-protocol'; import { chaiSetup, constants, @@ -109,7 +109,7 @@ describe(ContractName.DutchAuction, () => { const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( - coreArtifacts.Exchange, + protocolArtifacts.Exchange, provider, txDefaults, zrxAssetData, diff --git a/contracts/extensions/test/extensions/forwarder.ts b/contracts/extensions/test/extensions/forwarder.ts index 245d8eab9..4027f493d 100644 --- a/contracts/extensions/test/extensions/forwarder.ts +++ b/contracts/extensions/test/extensions/forwarder.ts @@ -1,10 +1,10 @@ import { - artifacts as coreArtifacts, + artifacts as protocolArtifacts, ERC20Wrapper, ERC721Wrapper, ExchangeContract, ExchangeWrapper, -} from '@0x/contracts-core'; +} from '@0x/contracts-protocol'; import { chaiSetup, constants, @@ -108,7 +108,7 @@ describe(ContractName.Forwarder, () => { wethAssetData = assetDataUtils.encodeERC20AssetData(wethContract.address); zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( - coreArtifacts.Exchange, + protocolArtifacts.Exchange, provider, txDefaults, zrxAssetData, @@ -179,7 +179,7 @@ describe(ContractName.Forwarder, () => { describe('constructor', () => { it('should revert if assetProxy is unregistered', async () => { const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( - coreArtifacts.Exchange, + protocolArtifacts.Exchange, provider, txDefaults, zrxAssetData, diff --git a/contracts/extensions/test/extensions/order_validator.ts b/contracts/extensions/test/extensions/order_validator.ts index 9a1dc0636..f0f3b43c7 100644 --- a/contracts/extensions/test/extensions/order_validator.ts +++ b/contracts/extensions/test/extensions/order_validator.ts @@ -1,12 +1,12 @@ import { - artifacts as coreArtifacts, + artifacts as protocolArtifacts, ERC20ProxyContract, ERC20Wrapper, ERC721ProxyContract, ERC721Wrapper, ExchangeContract, ExchangeWrapper, -} from '@0x/contracts-core'; +} from '@0x/contracts-protocol'; import { chaiSetup, constants, @@ -81,7 +81,7 @@ describe('OrderValidator', () => { const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); exchange = await ExchangeContract.deployFrom0xArtifactAsync( - coreArtifacts.Exchange, + protocolArtifacts.Exchange, provider, txDefaults, zrxAssetData, diff --git a/contracts/extensions/test/utils/forwarder_wrapper.ts b/contracts/extensions/test/utils/forwarder_wrapper.ts index 767a81ca6..9e44ff6b9 100644 --- a/contracts/extensions/test/utils/forwarder_wrapper.ts +++ b/contracts/extensions/test/utils/forwarder_wrapper.ts @@ -1,4 +1,4 @@ -import { artifacts as coreArtifacts } from '@0x/contracts-core'; +import { artifacts as protocolArtifacts } from '@0x/contracts-protocol'; import { constants, formatters, LogDecoder, MarketSellOrders } from '@0x/contracts-test-utils'; import { artifacts as tokensArtifacts } from '@0x/contracts-tokens'; import { SignedOrder } from '@0x/types'; @@ -60,7 +60,11 @@ export class ForwarderWrapper { constructor(contractInstance: ForwarderContract, provider: Provider) { this._forwarderContract = contractInstance; this._web3Wrapper = new Web3Wrapper(provider); - this._logDecoder = new LogDecoder(this._web3Wrapper, { ...artifacts, ...tokensArtifacts, ...coreArtifacts }); + this._logDecoder = new LogDecoder(this._web3Wrapper, { + ...artifacts, + ...tokensArtifacts, + ...protocolArtifacts, + }); } public async marketSellOrdersWithEthAsync( orders: SignedOrder[], diff --git a/contracts/multisig/package.json b/contracts/multisig/package.json index 37d064fef..5b8b8dc14 100644 --- a/contracts/multisig/package.json +++ b/contracts/multisig/package.json @@ -42,7 +42,7 @@ "bugs": { "url": "https://github.com/0xProject/0x-monorepo/issues" }, - "homepage": "https://github.com/0xProject/0x-monorepo/contracts/core/README.md", + "homepage": "https://github.com/0xProject/0x-monorepo/contracts/multisig/README.md", "devDependencies": { "@0x/contracts-test-utils": "^1.0.0", "@0x/abi-gen": "^1.0.17", 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: + // / >= / + // AND + // / >= / + // 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).event === 'Transfer', + ); + expect(transferLogs.length).to.be.equal(2); + expect((transferLogs[0] as LogWithDecodedArgs).address).to.be.equal( + zrxToken.address, + ); + expect((transferLogs[0] as LogWithDecodedArgs).args._from).to.be.equal( + makerAddress, + ); + expect((transferLogs[0] as LogWithDecodedArgs).args._to).to.be.equal( + feeRecipientAddress, + ); + expect( + (transferLogs[0] as LogWithDecodedArgs).args._value, + ).to.be.bignumber.equal(signedOrder.makerFee); + expect((transferLogs[1] as LogWithDecodedArgs).address).to.be.equal( + zrxToken.address, + ); + expect((transferLogs[1] as LogWithDecodedArgs).args._from).to.be.equal( + makerAddress, + ); + expect((transferLogs[1] as LogWithDecodedArgs).args._to).to.be.equal( + feeRecipientAddress, + ); + expect( + (transferLogs[1] as LogWithDecodedArgs).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; + 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; + 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 { + 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 { + 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 { + 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 { + // 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 { + 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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const totalFilledAmount = takerAssetFilledAmount.add(orderTakerAssetFilledAmount); + if (totalFilledAmount.greaterThan(MAX_UINT256)) { + throw overflowErrorForSendTransaction; + } + return totalFilledAmount; + } + async function testUpdateFilledStateAsync( + takerAssetFilledAmount: BigNumber, + orderTakerAssetFilledAmount: BigNumber, + orderHash: string, + ): Promise { + 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..62579cdc7 --- /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; + 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; + 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; + 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; + 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; + 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; + + 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; + 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; + 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; + 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; + 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; + 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; + 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; + 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; + 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; + 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; + 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; + 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; + 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 { + 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 { + 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 { + 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 { + 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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + // 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 { + 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 { + 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 { + 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 { + const proxyAddress = (this._proxyContract as ERC721ProxyContract).address; + await this.approveAsync(proxyAddress, tokenAddress, tokenId); + } + public async approveProxyForAllAsync(tokenAddress: string, tokenId: BigNumber, isApproved: boolean): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const owner = await tokenContract.ownerOf.callAsync(tokenId); + return owner; + } + public async isOwnerAsync(userAddress: string, tokenAddress: string, tokenId: BigNumber): Promise { + 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 { + 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 { + 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 { + 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..075d2cb96 --- /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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const filledAmount = await this._exchange.filled.callAsync(orderHashHex); + return filledAmount; + } + public async isCancelledAsync(orderHashHex: string): Promise { + const isCancelled = await this._exchange.cancelled.callAsync(orderHashHex); + return isCancelled; + } + public async getOrderEpochAsync(makerAddress: string, senderAddress: string): Promise { + const orderEpoch = await this._exchange.orderEpoch.callAsync(makerAddress, senderAddress); + return orderEpoch; + } + public async getOrderInfoAsync(signedOrder: SignedOrder): Promise { + const orderInfo = (await this._exchange.getOrderInfo.callAsync(signedOrder)) as OrderInfo; + return orderInfo; + } + public async getOrdersInfoAsync(signedOrders: SignedOrder[]): Promise { + const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[]; + return ordersInfo; + } + public async matchOrdersAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + from: string, + ): Promise { + 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 { + 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, +): Promise { + 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 { + // 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 { + 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; + 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 { + 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 { + 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 { + 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 { + // 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 { + // 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 { + // 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 { + // 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 { + 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 { + // 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 { + const balance = await this._assetWrapper.getBalanceAsync(userAddress, assetData); + return balance; + } + public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise { + 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 { + const filledTakerAmount = new BigNumber(await this._exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash)); + return filledTakerAmount; + } + public async isOrderCancelledAsync(signedOrder: SignedOrder): Promise { + 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 + } +} -- cgit v1.2.3