diff options
Diffstat (limited to 'packages/contracts')
186 files changed, 20748 insertions, 3711 deletions
diff --git a/packages/contracts/.solhint.json b/packages/contracts/.solhint.json new file mode 100644 index 000000000..076afe9f3 --- /dev/null +++ b/packages/contracts/.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/packages/contracts/README.md b/packages/contracts/README.md index 7628b057b..2e6376f39 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -34,14 +34,16 @@ 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 -yarn build +PKG=contracts yarn build ``` -or +Or continuously rebuild on change: ```bash -yarn build:watch +PKG=contracts yarn watch ``` ### Clean @@ -58,17 +60,55 @@ yarn lint ### Run Tests -Before running the tests, you will need to spin up a [TestRPC](https://www.npmjs.com/package/ethereumjs-testrpc) instance. +```bash +yarn test +``` -In a separate terminal, start TestRPC (a convenience command is provided as part of the [0x.js monorepo](https://github.com/0xProject/0x-monorepo)) +#### Testing options + +###### Revert stack traces + +If you want to see helpful stack traces (incl. line number, code snippet) for smart contract reverts, run the tests with: -```bash -cd ../.. -yarn testrpc ``` +yarn test:trace +``` + +**Note:** This currently slows down the test runs and is therefore not enabled by default. + +###### Backing Ethereum node -Then in your main terminal run +By default, our tests run against an in-process [Ganache](https://github.com/trufflesuite/ganache-core) instance. In order to run the tests against [Geth](https://github.com/ethereum/go-ethereum), first follow the instructions in the README for the devnet package to start the devnet Geth node. Then run: ```bash -yarn test +TEST_PROVIDER=geth yarn test +``` + +###### Code coverage + +In order to see the Solidity code coverage output generated by `@0xproject/sol-cov`, run: + ``` +yarn test:coverage +``` + +###### Gas profiler + +In order to profile the gas costs for a specific smart contract call/transaction, you can run the tests in `profiler` mode. + +**Note:** Traces emitted by ganache have incorrect gas costs so we recommend using Geth for profiling. + +``` +TEST_PROVIDER=geth yarn test:profiler +``` + +You'll see a warning that you need to explicitly enable and disable the profiler before and after the block of code you want to profile. + +```typescript +import { profiler } from './utils/profiler'; +profiler.start(); +// Some call to a smart contract +profiler.stop(); +``` + +Without explicitly starting and stopping the profiler, the profiler output will be too busy, and therefore unusable. diff --git a/packages/contracts/compiler.json b/packages/contracts/compiler.json new file mode 100644 index 000000000..ad35fc5b3 --- /dev/null +++ b/packages/contracts/compiler.json @@ -0,0 +1,54 @@ +{ + "artifactsDir": "../migrations/artifacts/2.0.0", + "contractsDir": "src/contracts", + "compilerSettings": { + "optimizer": { + "enabled": true, + "runs": 1000000 + }, + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } + } + }, + "contracts": [ + "AssetProxyOwner", + "DummyERC20Token", + "DummyERC721Receiver", + "DummyERC721Token", + "ERC20Proxy", + "ERC20Token", + "ERC721Token", + "ERC721Proxy", + "Exchange", + "ExchangeWrapper", + "Forwarder", + "IAssetData", + "IAssetProxy", + "IValidator", + "IWallet", + "MixinAuthorizable", + "MultiSigWallet", + "MultiSigWalletWithTimeLock", + "TestAssetProxyOwner", + "TestAssetProxyDispatcher", + "TestConstants", + "TestLibBytes", + "TestLibs", + "TestExchangeInternals", + "TestSignatureValidator", + "TokenRegistry", + "Validator", + "Wallet", + "WETH9", + "Whitelist", + "ZRXToken" + ] +} diff --git a/packages/contracts/coverage/.gitkeep b/packages/contracts/coverage/.gitkeep deleted file mode 100644 index e69de29bb..000000000 --- a/packages/contracts/coverage/.gitkeep +++ /dev/null diff --git a/packages/contracts/globals.d.ts b/packages/contracts/globals.d.ts index c6597054a..94e63a32d 100644 --- a/packages/contracts/globals.d.ts +++ b/packages/contracts/globals.d.ts @@ -1,35 +1,6 @@ -declare module 'chai-bignumber'; -declare module 'dirty-chai'; - -// HACK: In order to merge the bignumber declaration added by chai-bignumber to the chai Assertion -// interface we must use `namespace` as the Chai definitelyTyped definition does. Since we otherwise -// disallow `namespace`, we disable tslint for the following. -/* tslint:disable */ -declare namespace Chai { - interface Assertion { - bignumber: Assertion; - } -} -/* tslint:enable */ - declare module '*.json' { const json: any; /* tslint:disable */ export default json; /* tslint:enable */ } - -declare module 'solc' { - export function compile(sources: any, optimizerEnabled: number, findImports: (importPath: string) => any): any; - export function setupMethods(solcBin: any): any; -} - -declare module 'web3-eth-abi' { - export function encodeParameters(typesArray: string[], parameters: any[]): string; -} - -declare module 'ethereumjs-abi' { - const soliditySHA3: (argTypes: string[], args: any[]) => Buffer; - const soliditySHA256: (argTypes: string[], args: any[]) => Buffer; - const methodID: (name: string, types: string[]) => Buffer; -} diff --git a/packages/contracts/globalsAugment.d.ts b/packages/contracts/globalsAugment.d.ts deleted file mode 100644 index 9b16ce2ad..000000000 --- a/packages/contracts/globalsAugment.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BigNumber } from '@0xproject/utils'; - -// HACK: This module overrides the Chai namespace so that we can use BigNumber types inside. -// Source: https://github.com/Microsoft/TypeScript/issues/7352#issuecomment-191547232 -declare global { - // HACK: In order to merge the bignumber declaration added by chai-bignumber to the chai Assertion - // interface we must use `namespace` as the Chai definitelyTyped definition does. Since we otherwise - // disallow `namespace`, we disable tslint for the following. - /* tslint:disable */ - namespace Chai { - interface NumberComparer { - (value: number | BigNumber, message?: string): Assertion; - } - interface NumericComparison { - greaterThan: NumberComparer; - } - } - /* tslint:enable */ -} diff --git a/packages/contracts/migrations/config/multisig_sample.ts b/packages/contracts/migrations/config/multisig_sample.ts deleted file mode 100644 index bc2502fca..000000000 --- a/packages/contracts/migrations/config/multisig_sample.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { MultiSigConfigByNetwork } from '../types'; - -// Make a copy of this file named `multisig.js` and input custom params as needed -export const multiSig: MultiSigConfigByNetwork = { - kovan: { - owners: [], - confirmationsRequired: 0, - secondsRequired: 0, - }, -}; diff --git a/packages/contracts/migrations/config/token_info.ts b/packages/contracts/migrations/config/token_info.ts deleted file mode 100644 index 5fd3db144..000000000 --- a/packages/contracts/migrations/config/token_info.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { constants } from '../../util/constants'; -import { Token } from '../types'; - -export const tokenInfo: Token[] = [ - { - name: 'Augur Reputation Token', - symbol: 'REP', - decimals: 18, - ipfsHash: constants.NULL_BYTES, - swarmHash: constants.NULL_BYTES, - }, - { - name: 'Digix DAO Token', - symbol: 'DGD', - decimals: 18, - ipfsHash: constants.NULL_BYTES, - swarmHash: constants.NULL_BYTES, - }, - { - name: 'Golem Network Token', - symbol: 'GNT', - decimals: 18, - ipfsHash: constants.NULL_BYTES, - swarmHash: constants.NULL_BYTES, - }, - { - name: 'MakerDAO', - symbol: 'MKR', - decimals: 18, - ipfsHash: constants.NULL_BYTES, - swarmHash: constants.NULL_BYTES, - }, - { - name: 'Melon Token', - symbol: 'MLN', - decimals: 18, - ipfsHash: constants.NULL_BYTES, - swarmHash: constants.NULL_BYTES, - }, -]; diff --git a/packages/contracts/migrations/index.ts b/packages/contracts/migrations/index.ts deleted file mode 100644 index 47ae7044d..000000000 --- a/packages/contracts/migrations/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Deployer } from '@0xproject/deployer'; -import { devConstants } from '@0xproject/dev-utils'; -import * as path from 'path'; - -import { constants } from '../util/constants'; - -import { runMigrationsAsync } from './migrate'; - -const deployerOpts = { - artifactsDir: path.resolve('src', 'artifacts'), - jsonrpcUrl: devConstants.RPC_URL, - networkId: constants.TESTRPC_NETWORK_ID, - defaults: { - gas: devConstants.GAS_ESTIMATE, - }, -}; - -export const deployer = new Deployer(deployerOpts); - -runMigrationsAsync(deployer).catch(console.log); diff --git a/packages/contracts/migrations/migrate.ts b/packages/contracts/migrations/migrate.ts deleted file mode 100644 index cff8af0ab..000000000 --- a/packages/contracts/migrations/migrate.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Deployer } from '@0xproject/deployer'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as _ from 'lodash'; - -import { constants } from '../util/constants'; -import { ContractName } from '../util/types'; - -import { tokenInfo } from './config/token_info'; - -/** - * Custom migrations should be defined in this function. This will be called with the CLI 'migrate' command. - * Migrations could be written to run in parallel, but if you want contract addresses to be created deterministically, - * the migration should be written to run synchronously. - * @param deployer Deployer instance. - */ -export const runMigrationsAsync = async (deployer: Deployer) => { - const web3Wrapper: Web3Wrapper = deployer.web3Wrapper; - const accounts: string[] = await web3Wrapper.getAvailableAddressesAsync(); - - const tokenTransferProxy = await deployer.deployAndSaveAsync(ContractName.TokenTransferProxy); - const zrxToken = await deployer.deployAndSaveAsync(ContractName.ZRXToken); - const etherToken = await deployer.deployAndSaveAsync(ContractName.EtherToken); - const tokenReg = await deployer.deployAndSaveAsync(ContractName.TokenRegistry); - - const exchangeArgs = [zrxToken.address, tokenTransferProxy.address]; - const owners = [accounts[0], accounts[1]]; - const confirmationsRequired = new BigNumber(2); - const secondsRequired = new BigNumber(0); - const multiSigArgs = [owners, confirmationsRequired, secondsRequired, tokenTransferProxy.address]; - const exchange = await deployer.deployAndSaveAsync(ContractName.Exchange, exchangeArgs); - const multiSig = await deployer.deployAndSaveAsync( - ContractName.MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress, - multiSigArgs, - ); - - const owner = accounts[0]; - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: owner }); - await tokenTransferProxy.transferOwnership.sendTransactionAsync(multiSig.address, { from: owner }); - const addTokenGasEstimate = await tokenReg.addToken.estimateGasAsync( - zrxToken.address, - tokenInfo[0].name, - tokenInfo[0].symbol, - tokenInfo[0].decimals, - tokenInfo[0].ipfsHash, - tokenInfo[0].swarmHash, - { from: owner }, - ); - await tokenReg.addToken.sendTransactionAsync( - zrxToken.address, - '0x Protocol Token', - 'ZRX', - 18, - constants.NULL_BYTES, - constants.NULL_BYTES, - { - from: owner, - gas: addTokenGasEstimate, - }, - ); - await tokenReg.addToken.sendTransactionAsync( - etherToken.address, - 'Ether Token', - 'WETH', - 18, - constants.NULL_BYTES, - constants.NULL_BYTES, - { - from: owner, - gas: addTokenGasEstimate, - }, - ); - for (const token of tokenInfo) { - const totalSupply = new BigNumber(0); - const args = [token.name, token.symbol, token.decimals, totalSupply]; - const dummyToken = await deployer.deployAsync(ContractName.DummyToken, args); - await tokenReg.addToken.sendTransactionAsync( - dummyToken.address, - token.name, - token.symbol, - token.decimals, - token.ipfsHash, - token.swarmHash, - { - from: owner, - gas: addTokenGasEstimate, - }, - ); - } -}; diff --git a/packages/contracts/migrations/types.ts b/packages/contracts/migrations/types.ts deleted file mode 100644 index 58d1e5b4f..000000000 --- a/packages/contracts/migrations/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface MultiSigConfig { - owners: string[]; - confirmationsRequired: number; - secondsRequired: number; -} - -export interface MultiSigConfigByNetwork { - [networkName: string]: MultiSigConfig; -} - -export interface Token { - address?: string; - name: string; - symbol: string; - decimals: number; - ipfsHash: string; - swarmHash: string; -} - -export interface TokenInfoByNetwork { - development: Token[]; - live: Token[]; -} diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 9d7a6c87c..799f72d9e 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,34 +1,39 @@ { "private": true, "name": "contracts", - "version": "2.1.20", + "version": "2.1.40", + "engines": { + "node": ">=6.12" + }, "description": "Smart contract components of 0x protocol", "main": "index.js", "directories": { "test": "test" }, "scripts": { - "build:watch": "tsc -w", - "prebuild": "run-s clean compile copy_artifacts generate_contract_wrappers", - "copy_artifacts": "copyfiles './src/artifacts/**/*' ./lib", - "build": "tsc", - "test": "run-s build run_mocha", + "watch_without_deps": "yarn pre_build && tsc -w", + "build": "yarn pre_build && tsc", + "pre_build": "run-s compile copy_artifacts generate_contract_wrappers", + "copy_artifacts": "copyfiles -u 4 '../migrations/artifacts/2.0.0/**/*' ./lib/artifacts;", + "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", - "run_mocha": "mocha 'lib/test/**/*.js' --timeout 100000 --bail --exit", - "compile:comment": "Yarn workspaces do not link binaries correctly so we need to reference them directly https://github.com/yarnpkg/yarn/issues/3846", - "compile": "node ../deployer/lib/src/cli.js compile --contracts ${npm_package_config_contracts} --contracts-dir src/contracts --artifacts-dir src/artifacts", - "clean": "shx rm -rf ./lib", - "generate_contract_wrappers": "node ../abi-gen/lib/index.js --abis ${npm_package_config_abis} --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers/generated --backend ethers && prettier --write 'src/contract_wrappers/generated/**.ts'", - "migrate": "yarn build && yarn compile && node ./lib/migrations/index.js", - "lint": "tslint --project . 'migrations/**/*.ts' 'test/**/*.ts' 'util/**/*.ts' 'deploy/**/*.ts'", + "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 src", + "clean": "shx rm -rf lib generated_contract_wrappers", + "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output generated_contract_wrappers --backend ethers", + "lint": "tslint --project . --exclude **/src/generated_contract_wrappers/**/* --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:coverage" + "test:circleci": "yarn test", + "lint-contracts": "solhint src/2.0.0/**/**/**/**/*.sol" }, "config": { - "abis": "src/artifacts/@(DummyToken|TokenTransferProxy|Exchange|TokenRegistry|MultiSigWallet|MultiSigWalletWithTimeLock|MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress|TokenRegistry|ZRXToken|Arbitrage|EtherDelta|AccountLevels).json", - "contracts": "Exchange,DummyToken,ZRXToken,Token,WETH9,TokenTransferProxy,MultiSigWallet,MultiSigWalletWithTimeLock,MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress,MaliciousToken,TokenRegistry,Arbitrage,EtherDelta,AccountLevels" + "abis": "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|Validator|Wallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" }, "repository": { "type": "git", @@ -41,46 +46,46 @@ }, "homepage": "https://github.com/0xProject/0x-monorepo/packages/contracts/README.md", "devDependencies": { - "@0xproject/dev-utils": "^0.3.3", - "@0xproject/tslint-config": "^0.4.13", - "@types/bluebird": "^3.5.3", + "@0xproject/abi-gen": "^1.0.5", + "@0xproject/dev-utils": "^1.0.4", + "@0xproject/sol-cov": "^2.0.0", + "@0xproject/subproviders": "^1.0.5", + "@0xproject/tslint-config": "^1.0.5", + "@types/bn.js": "^4.11.0", + "@types/ethereumjs-abi": "^0.6.0", "@types/lodash": "4.14.104", "@types/node": "^8.0.53", - "@types/request-promise-native": "^1.0.2", "@types/yargs": "^10.0.0", "chai": "^4.0.1", "chai-as-promised": "^7.1.0", "chai-bignumber": "^2.0.1", "copyfiles": "^1.2.0", "dirty-chai": "^2.0.1", - "mocha": "^4.0.1", + "make-promises-safe": "^1.1.0", + "mocha": "^4.1.0", "npm-run-all": "^4.1.2", - "prettier": "^1.11.1", "shx": "^0.2.2", - "solc": "^0.4.18", - "tslint": "5.8.0", - "types-bn": "^0.0.1", - "types-ethereumjs-util": "0xProject/types-ethereumjs-util", - "typescript": "2.7.1", + "solc": "^0.4.24", + "solhint": "^1.2.1", + "tslint": "5.11.0", + "typescript": "2.9.2", "yargs": "^10.0.3" }, "dependencies": { - "0x.js": "^0.34.0", - "@0xproject/deployer": "^0.3.4", - "@0xproject/json-schemas": "^0.7.18", - "@0xproject/types": "^0.4.2", - "@0xproject/typescript-typings": "^0.0.2", - "@0xproject/utils": "^0.4.4", - "@0xproject/web3-wrapper": "^0.4.0", - "bluebird": "^3.5.0", + "@0xproject/base-contract": "^2.0.0-rc.1", + "@0xproject/order-utils": "^1.0.1-rc.3", + "@0xproject/sol-compiler": "^1.0.5", + "@0xproject/types": "^1.0.1-rc.4", + "@0xproject/typescript-typings": "^1.0.4", + "@0xproject/utils": "^1.0.5", + "@0xproject/web3-wrapper": "^1.2.0", + "@types/js-combinatorics": "^0.5.29", "bn.js": "^4.11.8", - "ethereumjs-abi": "^0.6.4", + "ethereum-types": "^1.0.4", + "ethereumjs-abi": "0.6.5", "ethereumjs-util": "^5.1.1", - "ethers-contracts": "^2.2.1", - "isomorphic-fetch": "^2.2.1", - "lodash": "^4.17.4", - "request": "^2.81.0", - "web3": "^0.20.0", - "web3-eth-abi": "^1.0.0-beta.24" + "ethers": "3.0.22", + "js-combinatorics": "^0.5.3", + "lodash": "^4.17.5" } } diff --git a/packages/contracts/src/contracts/current/tutorials/Arbitrage/Arbitrage.sol b/packages/contracts/src/1.0.0/Arbitrage/Arbitrage.sol index a9f3c22e6..5054afc2f 100644 --- a/packages/contracts/src/contracts/current/tutorials/Arbitrage/Arbitrage.sol +++ b/packages/contracts/src/1.0.0/Arbitrage/Arbitrage.sol @@ -1,9 +1,9 @@ pragma solidity ^0.4.19; -import { Exchange } from "../../protocol/Exchange/Exchange.sol"; +import { IExchange_v1 as Exchange } from "../Exchange/IExchange_v1.sol"; import { EtherDelta } from "../EtherDelta/EtherDelta.sol"; -import { Ownable } from "../../utils/Ownable/Ownable.sol"; -import { Token } from "../../tokens/Token/Token.sol"; +import { Ownable_v1 as Ownable } from "../Ownable/Ownable_v1.sol"; +import { IToken_v1 as Token } from "../Token/IToken_v1.sol"; /// @title Arbitrage - Facilitates atomic arbitrage of ERC20 tokens between EtherDelta and 0x Exchange contract. /// @author Leonid Logvinov - <leo@0xProject.com> diff --git a/packages/contracts/src/contracts/previous/ERC20Token/ERC20Token_v1.sol b/packages/contracts/src/1.0.0/ERC20Token/ERC20Token_v1.sol index e05ee2d5e..e05ee2d5e 100644 --- a/packages/contracts/src/contracts/previous/ERC20Token/ERC20Token_v1.sol +++ b/packages/contracts/src/1.0.0/ERC20Token/ERC20Token_v1.sol diff --git a/packages/contracts/src/contracts/current/tutorials/EtherDelta/AccountLevels.sol b/packages/contracts/src/1.0.0/EtherDelta/AccountLevels.sol index 8d7a930d3..8d7a930d3 100644 --- a/packages/contracts/src/contracts/current/tutorials/EtherDelta/AccountLevels.sol +++ b/packages/contracts/src/1.0.0/EtherDelta/AccountLevels.sol diff --git a/packages/contracts/src/contracts/current/tutorials/EtherDelta/EtherDelta.sol b/packages/contracts/src/1.0.0/EtherDelta/EtherDelta.sol index 49847ab48..fe599ca0a 100644 --- a/packages/contracts/src/contracts/current/tutorials/EtherDelta/EtherDelta.sol +++ b/packages/contracts/src/1.0.0/EtherDelta/EtherDelta.sol @@ -1,8 +1,8 @@ pragma solidity ^0.4.19; -import { SafeMath } from "../../utils/SafeMath/SafeMath.sol"; +import { SafeMath } from "../SafeMath/SafeMath_v1.sol"; import { AccountLevels } from "./AccountLevels.sol"; -import { Token } from "../../tokens/Token/Token.sol"; +import { Token } from "../Token/Token_v1.sol"; contract EtherDelta is SafeMath { address public admin; //the admin address diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol b/packages/contracts/src/1.0.0/Exchange/Exchange_v1.sol index 8dacf797c..3f8e7368d 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol +++ b/packages/contracts/src/1.0.0/Exchange/Exchange_v1.sol @@ -1,6 +1,6 @@ /* - Copyright 2017 ZeroEx Intl. + 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. @@ -18,13 +18,13 @@ pragma solidity ^0.4.14; -import { TokenTransferProxy } from "../TokenTransferProxy/TokenTransferProxy.sol"; -import { Token_v1 as Token } from "../../../previous/Token/Token_v1.sol"; -import { SafeMath_v1 as SafeMath } from "../../../previous/SafeMath/SafeMath_v1.sol"; +import { TokenTransferProxy_v1 as TokenTransferProxy } from "../TokenTransferProxy/TokenTransferProxy_v1.sol"; +import { Token_v1 as Token } from "../Token/Token_v1.sol"; +import { SafeMath_v1 as SafeMath } from "../SafeMath/SafeMath_v1.sol"; /// @title Exchange - Facilitates exchange of ERC20 tokens. /// @author Amir Bandeali - <amir@0xProject.com>, Will Warren - <will@0xProject.com> -contract Exchange is SafeMath { +contract Exchange_v1 is SafeMath { // Error Codes enum Errors { @@ -85,7 +85,7 @@ contract Exchange is SafeMath { bytes32 orderHash; } - function Exchange(address _zrxToken, address _tokenTransferProxy) { + function Exchange_v1(address _zrxToken, address _tokenTransferProxy) { ZRX_TOKEN_CONTRACT = _zrxToken; TOKEN_TRANSFER_PROXY_CONTRACT = _tokenTransferProxy; } diff --git a/packages/contracts/src/1.0.0/Exchange/IExchange_v1.sol b/packages/contracts/src/1.0.0/Exchange/IExchange_v1.sol new file mode 100644 index 000000000..ec4bce7b0 --- /dev/null +++ b/packages/contracts/src/1.0.0/Exchange/IExchange_v1.sol @@ -0,0 +1,226 @@ +pragma solidity ^0.4.19; + +contract IExchange_v1 { + + // Error Codes + enum Errors { + ORDER_EXPIRED, // Order has already expired + ORDER_FULLY_FILLED_OR_CANCELLED, // Order has already been fully filled or cancelled + ROUNDING_ERROR_TOO_LARGE, // Rounding error too large + INSUFFICIENT_BALANCE_OR_ALLOWANCE // Insufficient balance or allowance for token transfer + } + + event LogError(uint8 indexed errorId, bytes32 indexed orderHash); + + event LogFill( + address indexed maker, + address taker, + address indexed feeRecipient, + address makerToken, + address takerToken, + uint filledMakerTokenAmount, + uint filledTakerTokenAmount, + uint paidMakerFee, + uint paidTakerFee, + bytes32 indexed tokens, // keccak256(makerToken, takerToken), allows subscribing to a token pair + bytes32 orderHash + ); + + event LogCancel( + address indexed maker, + address indexed feeRecipient, + address makerToken, + address takerToken, + uint cancelledMakerTokenAmount, + uint cancelledTakerTokenAmount, + bytes32 indexed tokens, + bytes32 orderHash + ); + + function ZRX_TOKEN_CONTRACT() + public view + returns (address); + + function TOKEN_TRANSFER_PROXY_CONTRACT() + public view + returns (address); + + function EXTERNAL_QUERY_GAS_LIMIT() + public view + returns (uint16); + + function VERSION() + public view + returns (string); + + function filled(bytes32) + public view + returns (uint256); + + function cancelled(bytes32) + public view + returns (uint256); + + /// @dev Calculates the sum of values already filled and cancelled for a given order. + /// @param orderHash The Keccak-256 hash of the given order. + /// @return Sum of values already filled and cancelled. + function getUnavailableTakerTokenAmount(bytes32 orderHash) + public constant + returns (uint); + + /// @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 getPartialAmount(uint numerator, uint denominator, uint target) + public constant + returns (uint); + + /// @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 isRoundingError(uint numerator, uint denominator, uint target) + public constant + returns (bool); + + /// @dev Calculates Keccak-256 hash of order with specified parameters. + /// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient. + /// @param orderValues Array of order's makerTokenAmount, takerTokenAmount, makerFee, takerFee, expirationTimestampInSec, and salt. + /// @return Keccak-256 hash of order. + function getOrderHash(address[5] orderAddresses, uint[6] orderValues) + public + constant + returns (bytes32); + + /// @dev Verifies that an order signature is valid. + /// @param signer address of signer. + /// @param hash Signed Keccak-256 hash. + /// @param v ECDSA signature parameter v. + /// @param r ECDSA signature parameters r. + /// @param s ECDSA signature parameters s. + /// @return Validity of order signature. + function isValidSignature( + address signer, + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s) + public constant + returns (bool); + + /// @dev Fills the input order. + /// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient. + /// @param orderValues Array of order's makerTokenAmount, takerTokenAmount, makerFee, takerFee, expirationTimestampInSec, and salt. + /// @param fillTakerTokenAmount Desired amount of takerToken to fill. + /// @param shouldThrowOnInsufficientBalanceOrAllowance Test if transfer will fail before attempting. + /// @param v ECDSA signature parameter v. + /// @param r ECDSA signature parameters r. + /// @param s ECDSA signature parameters s. + /// @return Total amount of takerToken filled in trade. + function fillOrder( + address[5] orderAddresses, + uint[6] orderValues, + uint fillTakerTokenAmount, + bool shouldThrowOnInsufficientBalanceOrAllowance, + uint8 v, + bytes32 r, + bytes32 s) + public + returns (uint filledTakerTokenAmount); + + /// @dev Cancels the input order. + /// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient. + /// @param orderValues Array of order's makerTokenAmount, takerTokenAmount, makerFee, takerFee, expirationTimestampInSec, and salt. + /// @param cancelTakerTokenAmount Desired amount of takerToken to cancel in order. + /// @return Amount of takerToken cancelled. + function cancelOrder( + address[5] orderAddresses, + uint[6] orderValues, + uint cancelTakerTokenAmount) + public + returns (uint); + + + /// @dev Fills an order with specified parameters and ECDSA signature, throws if specified amount not filled entirely. + /// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient. + /// @param orderValues Array of order's makerTokenAmount, takerTokenAmount, makerFee, takerFee, expirationTimestampInSec, and salt. + /// @param fillTakerTokenAmount Desired amount of takerToken to fill. + /// @param v ECDSA signature parameter v. + /// @param r ECDSA signature parameters r. + /// @param s ECDSA signature parameters s. + function fillOrKillOrder( + address[5] orderAddresses, + uint[6] orderValues, + uint fillTakerTokenAmount, + uint8 v, + bytes32 r, + bytes32 s) + public; + + /// @dev Synchronously executes multiple fill orders in a single transaction. + /// @param orderAddresses Array of address arrays containing individual order addresses. + /// @param orderValues Array of uint arrays containing individual order values. + /// @param fillTakerTokenAmounts Array of desired amounts of takerToken to fill in orders. + /// @param shouldThrowOnInsufficientBalanceOrAllowance Test if transfers will fail before attempting. + /// @param v Array ECDSA signature v parameters. + /// @param r Array of ECDSA signature r parameters. + /// @param s Array of ECDSA signature s parameters. + function batchFillOrders( + address[5][] orderAddresses, + uint[6][] orderValues, + uint[] fillTakerTokenAmounts, + bool shouldThrowOnInsufficientBalanceOrAllowance, + uint8[] v, + bytes32[] r, + bytes32[] s) + public; + + /// @dev Synchronously executes multiple fillOrKill orders in a single transaction. + /// @param orderAddresses Array of address arrays containing individual order addresses. + /// @param orderValues Array of uint arrays containing individual order values. + /// @param fillTakerTokenAmounts Array of desired amounts of takerToken to fill in orders. + /// @param v Array ECDSA signature v parameters. + /// @param r Array of ECDSA signature r parameters. + /// @param s Array of ECDSA signature s parameters. + function batchFillOrKillOrders( + address[5][] orderAddresses, + uint[6][] orderValues, + uint[] fillTakerTokenAmounts, + uint8[] v, + bytes32[] r, + bytes32[] s) + public; + + /// @dev Synchronously executes multiple fill orders in a single transaction until total fillTakerTokenAmount filled. + /// @param orderAddresses Array of address arrays containing individual order addresses. + /// @param orderValues Array of uint arrays containing individual order values. + /// @param fillTakerTokenAmount Desired total amount of takerToken to fill in orders. + /// @param shouldThrowOnInsufficientBalanceOrAllowance Test if transfers will fail before attempting. + /// @param v Array ECDSA signature v parameters. + /// @param r Array of ECDSA signature r parameters. + /// @param s Array of ECDSA signature s parameters. + /// @return Total amount of fillTakerTokenAmount filled in orders. + function fillOrdersUpTo( + address[5][] orderAddresses, + uint[6][] orderValues, + uint fillTakerTokenAmount, + bool shouldThrowOnInsufficientBalanceOrAllowance, + uint8[] v, + bytes32[] r, + bytes32[] s) + public + returns (uint); + + /// @dev Synchronously cancels multiple orders in a single transaction. + /// @param orderAddresses Array of address arrays containing individual order addresses. + /// @param orderValues Array of uint arrays containing individual order values. + /// @param cancelTakerTokenAmounts Array of desired amounts of takerToken to cancel in orders. + function batchCancelOrders( + address[5][] orderAddresses, + uint[6][] orderValues, + uint[] cancelTakerTokenAmounts) + public; +} diff --git a/packages/contracts/src/contracts/current/multisig/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.sol b/packages/contracts/src/1.0.0/MultiSigWalletWithTImeLockExceptRemoveAuthorizedAddress/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.sol index 3c6a3d2ef..aee722c53 100644 --- a/packages/contracts/src/contracts/current/multisig/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.sol +++ b/packages/contracts/src/1.0.0/MultiSigWalletWithTImeLockExceptRemoveAuthorizedAddress/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.sol @@ -1,6 +1,6 @@ /* - Copyright 2017 ZeroEx Intl. + 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. @@ -18,7 +18,7 @@ pragma solidity ^0.4.10; -import { MultiSigWalletWithTimeLock } from "../MultiSigWalletWithTimeLock/MultiSigWalletWithTimeLock.sol"; +import "../../2.0.0/multisig/MultiSigWalletWithTimeLock.sol"; contract MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress is MultiSigWalletWithTimeLock { diff --git a/packages/contracts/src/1.0.0/Ownable/IOwnable_v1.sol b/packages/contracts/src/1.0.0/Ownable/IOwnable_v1.sol new file mode 100644 index 000000000..7e22d544d --- /dev/null +++ b/packages/contracts/src/1.0.0/Ownable/IOwnable_v1.sol @@ -0,0 +1,18 @@ +pragma solidity ^0.4.19; + +/* + * Ownable + * + * Base contract with an owner. + * Provides onlyOwner modifier, which prevents function from running if it is called by anyone other than the owner. + */ + +contract IOwnable_v1 { + + function owner() + public view + returns (address); + + function transferOwnership(address newOwner) + public; +} diff --git a/packages/contracts/src/contracts/previous/Ownable/Ownable_v1.sol b/packages/contracts/src/1.0.0/Ownable/Ownable_v1.sol index c87438fa4..c87438fa4 100644 --- a/packages/contracts/src/contracts/previous/Ownable/Ownable_v1.sol +++ b/packages/contracts/src/1.0.0/Ownable/Ownable_v1.sol diff --git a/packages/contracts/src/contracts/previous/SafeMath/SafeMath_v1.sol b/packages/contracts/src/1.0.0/SafeMath/SafeMath_v1.sol index 341d611ec..341d611ec 100644 --- a/packages/contracts/src/contracts/previous/SafeMath/SafeMath_v1.sol +++ b/packages/contracts/src/1.0.0/SafeMath/SafeMath_v1.sol diff --git a/packages/contracts/src/contracts/previous/Token/Token_v1.sol b/packages/contracts/src/1.0.0/Token/Token_v1.sol index de619fb7e..de619fb7e 100644 --- a/packages/contracts/src/contracts/previous/Token/Token_v1.sol +++ b/packages/contracts/src/1.0.0/Token/Token_v1.sol diff --git a/packages/contracts/src/1.0.0/TokenRegistry/ITokenRegistery.sol b/packages/contracts/src/1.0.0/TokenRegistry/ITokenRegistery.sol new file mode 100644 index 000000000..b8bdaf3b9 --- /dev/null +++ b/packages/contracts/src/1.0.0/TokenRegistry/ITokenRegistery.sol @@ -0,0 +1,195 @@ +/* + + 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.21; + +import { IOwnable_v1 as IOwnable } from "../Ownable/IOwnable_v1.sol"; + +/// @title Token Registry - Stores metadata associated with ERC20 tokens. See ERC22 https://github.com/ethereum/EIPs/issues/22 +/// @author Amir Bandeali - <amir@0xProject.com>, Will Warren - <will@0xProject.com> +contract ITokenRegistery is IOwnable { + + event LogAddToken( + address indexed token, + string name, + string symbol, + uint8 decimals, + bytes ipfsHash, + bytes swarmHash + ); + + event LogRemoveToken( + address indexed token, + string name, + string symbol, + uint8 decimals, + bytes ipfsHash, + bytes swarmHash + ); + + event LogTokenNameChange( + address indexed token, + string oldName, + string newName + ); + + event LogTokenSymbolChange( + address indexed token, + string oldSymbol, + string newSymbol + ); + + event LogTokenIpfsHashChange( + address indexed token, + bytes oldIpfsHash, + bytes newIpfsHash + ); + + event LogTokenSwarmHashChange( + address indexed token, + bytes oldSwarmHash, + bytes newSwarmHash + ); + + function tokens(address tokenAddress) + public view + returns ( + address token, + string name, + string symbol, + uint8 decimals, + bytes ipfsHash, + bytes swarmHash + ); + + function tokenAddresses(uint256 index) + public view + returns (address); + + + /// @dev Allows owner to add a new token to the registry. + /// @param _token Address of new token. + /// @param _name Name of new token. + /// @param _symbol Symbol for new token. + /// @param _decimals Number of decimals, divisibility of new token. + /// @param _ipfsHash IPFS hash of token icon. + /// @param _swarmHash Swarm hash of token icon. + function addToken( + address _token, + string _name, + string _symbol, + uint8 _decimals, + bytes _ipfsHash, + bytes _swarmHash) + public; + + /// @dev Allows owner to remove an existing token from the registry. + /// @param _token Address of existing token. + function removeToken(address _token, uint _index) + public; + + /// @dev Allows owner to modify an existing token's name. + /// @param _token Address of existing token. + /// @param _name New name. + function setTokenName(address _token, string _name) + public; + + /// @dev Allows owner to modify an existing token's symbol. + /// @param _token Address of existing token. + /// @param _symbol New symbol. + function setTokenSymbol(address _token, string _symbol) + public; + + /// @dev Allows owner to modify an existing token's IPFS hash. + /// @param _token Address of existing token. + /// @param _ipfsHash New IPFS hash. + function setTokenIpfsHash(address _token, bytes _ipfsHash) + public; + + /// @dev Allows owner to modify an existing token's Swarm hash. + /// @param _token Address of existing token. + /// @param _swarmHash New Swarm hash. + function setTokenSwarmHash(address _token, bytes _swarmHash) + public; + + /* + * Web3 call functions + */ + + /// @dev Provides a registered token's address when given the token symbol. + /// @param _symbol Symbol of registered token. + /// @return Token's address. + function getTokenAddressBySymbol(string _symbol) + public constant + returns (address); + + /// @dev Provides a registered token's address when given the token name. + /// @param _name Name of registered token. + /// @return Token's address. + function getTokenAddressByName(string _name) + public constant + returns (address); + + /// @dev Provides a registered token's metadata, looked up by address. + /// @param _token Address of registered token. + /// @return Token metadata. + function getTokenMetaData(address _token) + public constant + returns ( + address, //tokenAddress + string, //name + string, //symbol + uint8, //decimals + bytes, //ipfsHash + bytes //swarmHash + ); + + /// @dev Provides a registered token's metadata, looked up by name. + /// @param _name Name of registered token. + /// @return Token metadata. + function getTokenByName(string _name) + public constant + returns ( + address, //tokenAddress + string, //name + string, //symbol + uint8, //decimals + bytes, //ipfsHash + bytes //swarmHash + ); + + /// @dev Provides a registered token's metadata, looked up by symbol. + /// @param _symbol Symbol of registered token. + /// @return Token metadata. + function getTokenBySymbol(string _symbol) + public constant + returns ( + address, //tokenAddress + string, //name + string, //symbol + uint8, //decimals + bytes, //ipfsHash + bytes //swarmHash + ); + + /// @dev Returns an array containing all token addresses. + /// @return Array of token addresses. + function getTokenAddresses() + public constant + returns (address[]); +} diff --git a/packages/contracts/src/contracts/current/protocol/TokenRegistry/TokenRegistry.sol b/packages/contracts/src/1.0.0/TokenRegistry/TokenRegistry.sol index 3bd2fbfaf..7417a10a3 100644 --- a/packages/contracts/src/contracts/current/protocol/TokenRegistry/TokenRegistry.sol +++ b/packages/contracts/src/1.0.0/TokenRegistry/TokenRegistry.sol @@ -1,6 +1,6 @@ /* - Copyright 2017 ZeroEx Intl. + 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. @@ -18,7 +18,7 @@ pragma solidity ^0.4.11; -import { Ownable_v1 as Ownable } from "../../../previous/Ownable/Ownable_v1.sol"; +import { Ownable_v1 as Ownable } from "../Ownable/Ownable_v1.sol"; /// @title Token Registry - Stores metadata associated with ERC20 tokens. See ERC22 https://github.com/ethereum/EIPs/issues/22 /// @author Amir Bandeali - <amir@0xProject.com>, Will Warren - <will@0xProject.com> diff --git a/packages/contracts/src/contracts/current/protocol/TokenTransferProxy/TokenTransferProxy.sol b/packages/contracts/src/1.0.0/TokenTransferProxy/TokenTransferProxy_v1.sol index 1ce949fa6..e3659d8ba 100644 --- a/packages/contracts/src/contracts/current/protocol/TokenTransferProxy/TokenTransferProxy.sol +++ b/packages/contracts/src/1.0.0/TokenTransferProxy/TokenTransferProxy_v1.sol @@ -1,6 +1,6 @@ /* - Copyright 2017 ZeroEx Intl. + 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. @@ -18,12 +18,12 @@ pragma solidity ^0.4.11; -import { Token_v1 as Token } from "../../../previous/Token/Token_v1.sol"; -import { Ownable_v1 as Ownable } from "../../../previous/Ownable/Ownable_v1.sol"; +import { Token_v1 as Token } from "../Token/Token_v1.sol"; +import { Ownable_v1 as Ownable } from "../Ownable/Ownable_v1.sol"; /// @title TokenTransferProxy - Transfers tokens on behalf of contracts that have been approved via decentralized governance. /// @author Amir Bandeali - <amir@0xProject.com>, Will Warren - <will@0xProject.com> -contract TokenTransferProxy is Ownable { +contract TokenTransferProxy_v1 is Ownable { /// @dev Only authorized addresses can invoke functions with this modifier. modifier onlyAuthorized { diff --git a/packages/contracts/src/contracts/previous/UnlimitedAllowanceToken/UnlimitedAllowanceToken_v1.sol b/packages/contracts/src/1.0.0/UnlimitedAllowanceToken/UnlimitedAllowanceToken_v1.sol index 6376f3f2c..46379c43d 100644 --- a/packages/contracts/src/contracts/previous/UnlimitedAllowanceToken/UnlimitedAllowanceToken_v1.sol +++ b/packages/contracts/src/1.0.0/UnlimitedAllowanceToken/UnlimitedAllowanceToken_v1.sol @@ -1,6 +1,6 @@ /* - Copyright 2017 ZeroEx Intl. + 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. diff --git a/packages/contracts/src/2.0.0/examples/ExchangeWrapper/ExchangeWrapper.sol b/packages/contracts/src/2.0.0/examples/ExchangeWrapper/ExchangeWrapper.sol new file mode 100644 index 000000000..2fa0e3c5e --- /dev/null +++ b/packages/contracts/src/2.0.0/examples/ExchangeWrapper/ExchangeWrapper.sol @@ -0,0 +1,100 @@ +/* + + 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/interfaces/IExchange.sol"; +import "../../protocol/Exchange/libs/LibOrder.sol"; + + +contract ExchangeWrapper { + + // Exchange contract. + // solhint-disable-next-line var-name-mixedcase + IExchange internal EXCHANGE; + + constructor (address _exchange) + public + { + EXCHANGE = IExchange(_exchange); + } + + /// @dev Cancels all orders created by sender with a salt less than or equal to the targetOrderEpoch + /// and senderAddress equal to this contract. + /// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled. + /// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash. + /// @param makerSignature Proof that maker wishes to call this function with given params. + function cancelOrdersUpTo( + uint256 targetOrderEpoch, + uint256 salt, + bytes makerSignature + ) + external + { + address makerAddress = msg.sender; + + // Encode arguments into byte array. + bytes memory data = abi.encodeWithSelector( + EXCHANGE.cancelOrdersUpTo.selector, + targetOrderEpoch + ); + + // Call `cancelOrdersUpTo` via `executeTransaction`. + EXCHANGE.executeTransaction( + salt, + makerAddress, + data, + makerSignature + ); + } + + /// @dev Fills an order using `msg.sender` as the taker. + /// @param order Order struct containing order specifications. + /// @param takerAssetFillAmount Desired amount of takerAsset to sell. + /// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash. + /// @param orderSignature Proof that order has been created by maker. + /// @param takerSignature Proof that taker wishes to call this function with given params. + function fillOrder( + LibOrder.Order memory order, + uint256 takerAssetFillAmount, + uint256 salt, + bytes memory orderSignature, + bytes memory takerSignature + ) + public + { + address takerAddress = msg.sender; + + // Encode arguments into byte array. + bytes memory data = abi.encodeWithSelector( + EXCHANGE.fillOrder.selector, + order, + takerAssetFillAmount, + orderSignature + ); + + // Call `fillOrder` via `executeTransaction`. + EXCHANGE.executeTransaction( + salt, + takerAddress, + data, + takerSignature + ); + } +} diff --git a/packages/contracts/src/2.0.0/examples/Validator/Validator.sol b/packages/contracts/src/2.0.0/examples/Validator/Validator.sol new file mode 100644 index 000000000..72ed528ba --- /dev/null +++ b/packages/contracts/src/2.0.0/examples/Validator/Validator.sol @@ -0,0 +1,56 @@ +/* + + 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/interfaces/IValidator.sol"; + + +contract Validator is + IValidator +{ + + // The single valid signer for this wallet. + // solhint-disable-next-line var-name-mixedcase + address internal VALID_SIGNER; + + /// @dev constructs a new `Validator` with a single valid signer. + /// @param validSigner The sole, valid signer. + constructor (address validSigner) public { + VALID_SIGNER = validSigner; + } + + /// @dev Verifies that a signature is valid. `signer` must match `VALID_SIGNER`. + /// @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 signature. + // solhint-disable no-unused-vars + function isValidSignature( + bytes32 hash, + address signerAddress, + bytes signature + ) + external + view + returns (bool isValid) + { + return (signerAddress == VALID_SIGNER); + } + // solhint-enable no-unused-vars +} diff --git a/packages/contracts/src/2.0.0/examples/Wallet/Wallet.sol b/packages/contracts/src/2.0.0/examples/Wallet/Wallet.sol new file mode 100644 index 000000000..b75021a31 --- /dev/null +++ b/packages/contracts/src/2.0.0/examples/Wallet/Wallet.sol @@ -0,0 +1,65 @@ +/* + + 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/interfaces/IWallet.sol"; +import "../../utils/LibBytes/LibBytes.sol"; + + +contract Wallet is + IWallet +{ + using LibBytes for bytes; + + // The owner of this wallet. + // solhint-disable-next-line var-name-mixedcase + address internal WALLET_OWNER; + + /// @dev constructs a new `Wallet` with a single owner. + /// @param walletOwner The owner of this wallet. + constructor (address walletOwner) public { + WALLET_OWNER = walletOwner; + } + + /// @dev Validates an EIP712 signature. + /// The signer must match the owner of this wallet. + /// @param hash Message hash that is signed. + /// @param eip712Signature Proof of signing. + /// @return Validity of signature. + function isValidSignature( + bytes32 hash, + bytes eip712Signature + ) + external + view + returns (bool isValid) + { + require( + eip712Signature.length == 65, + "LENGTH_65_REQUIRED" + ); + + uint8 v = uint8(eip712Signature[0]); + bytes32 r = eip712Signature.readBytes32(1); + bytes32 s = eip712Signature.readBytes32(33); + address recoveredAddress = ecrecover(hash, v, r, s); + isValid = WALLET_OWNER == recoveredAddress; + return isValid; + } +} diff --git a/packages/contracts/src/2.0.0/examples/Whitelist/Whitelist.sol b/packages/contracts/src/2.0.0/examples/Whitelist/Whitelist.sol new file mode 100644 index 000000000..60cac26ea --- /dev/null +++ b/packages/contracts/src/2.0.0/examples/Whitelist/Whitelist.sol @@ -0,0 +1,136 @@ +/* + + 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/interfaces/IExchange.sol"; +import "../../protocol/Exchange/libs/LibOrder.sol"; +import "../../utils/Ownable/Ownable.sol"; + + +contract Whitelist is + Ownable +{ + + // Mapping of address => whitelist status. + mapping (address => bool) public isWhitelisted; + + // Exchange contract. + // solhint-disable var-name-mixedcase + IExchange internal EXCHANGE; + bytes internal TX_ORIGIN_SIGNATURE; + // solhint-enable var-name-mixedcase + + byte constant internal VALIDATOR_SIGNATURE_BYTE = "\x06"; + + constructor (address _exchange) + public + { + EXCHANGE = IExchange(_exchange); + TX_ORIGIN_SIGNATURE = abi.encodePacked(address(this), VALIDATOR_SIGNATURE_BYTE); + } + + /// @dev Adds or removes an address from the whitelist. + /// @param target Address to add or remove from whitelist. + /// @param isApproved Whitelist status to assign to address. + function updateWhitelistStatus( + address target, + bool isApproved + ) + external + onlyOwner + { + isWhitelisted[target] = isApproved; + } + + /// @dev Verifies signer is same as signer of current Ethereum transaction. + /// NOTE: This function can currently be used to validate signatures coming from outside of this contract. + /// Extra safety checks can be added for a production contract. + /// @param signerAddress Address that should have signed the given hash. + /// @param signature Proof of signing. + /// @return Validity of order signature. + // solhint-disable no-unused-vars + function isValidSignature( + bytes32 hash, + address signerAddress, + bytes signature + ) + external + view + returns (bool isValid) + { + // solhint-disable-next-line avoid-tx-origin + return signerAddress == tx.origin; + } + // solhint-enable no-unused-vars + + /// @dev Fills an order using `msg.sender` as the taker. + /// The transaction will revert if both the maker and taker are not whitelisted. + /// Orders should specify this contract as the `senderAddress` in order to gaurantee + /// that both maker and taker have been whitelisted. + /// @param order Order struct containing order specifications. + /// @param takerAssetFillAmount Desired amount of takerAsset to sell. + /// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash. + /// @param orderSignature Proof that order has been created by maker. + function fillOrderIfWhitelisted( + LibOrder.Order memory order, + uint256 takerAssetFillAmount, + uint256 salt, + bytes memory orderSignature + ) + public + { + address takerAddress = msg.sender; + + // This contract must be the entry point for the transaction. + require( + // solhint-disable-next-line avoid-tx-origin + takerAddress == tx.origin, + "INVALID_SENDER" + ); + + // Check if maker is on the whitelist. + require( + isWhitelisted[order.makerAddress], + "MAKER_NOT_WHITELISTED" + ); + + // Check if taker is on the whitelist. + require( + isWhitelisted[takerAddress], + "TAKER_NOT_WHITELISTED" + ); + + // Encode arguments into byte array. + bytes memory data = abi.encodeWithSelector( + EXCHANGE.fillOrder.selector, + order, + takerAssetFillAmount, + orderSignature + ); + + // Call `fillOrder` via `executeTransaction`. + EXCHANGE.executeTransaction( + salt, + takerAddress, + data, + TX_ORIGIN_SIGNATURE + ); + } +} diff --git a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol new file mode 100644 index 000000000..5b88b05b1 --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol @@ -0,0 +1,55 @@ +/* + + 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 "./MixinWeth.sol"; +import "./MixinForwarderCore.sol"; +import "./libs/LibConstants.sol"; +import "./MixinAssets.sol"; +import "./MixinExchangeWrapper.sol"; + + +// solhint-disable no-empty-blocks +contract Forwarder is + LibConstants, + MixinWeth, + MixinAssets, + MixinExchangeWrapper, + MixinForwarderCore +{ + + constructor ( + address _exchange, + address _etherToken, + address _zrxToken, + bytes memory _zrxAssetData, + bytes memory _wethAssetData + ) + public + LibConstants( + _exchange, + _etherToken, + _zrxToken, + _zrxAssetData, + _wethAssetData + ) + MixinForwarderCore() + {} +} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol new file mode 100644 index 000000000..e06f9a8e3 --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/MixinAssets.sol @@ -0,0 +1,144 @@ +/* + + 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 "../utils/LibBytes/LibBytes.sol"; +import "../utils/Ownable/Ownable.sol"; +import "../tokens/ERC20Token/IERC20Token.sol"; +import "../tokens/ERC721Token/IERC721Token.sol"; +import "./libs/LibConstants.sol"; +import "./mixins/MAssets.sol"; + + +contract MixinAssets is + Ownable, + LibConstants, + MAssets +{ + + using LibBytes for bytes; + + bytes4 constant internal ERC20_TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)")); + + /// @dev Withdraws assets from this contract. The contract requires a ZRX balance in order to + /// function optimally, and this function allows the ZRX to be withdrawn by owner. It may also be + /// used to withdraw assets that were accidentally sent to this contract. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of ERC20 token to withdraw. + function withdrawAsset( + bytes assetData, + uint256 amount + ) + external + onlyOwner + { + transferAssetToSender(assetData, amount); + } + + /// @dev Transfers given amount of asset to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferAssetToSender( + bytes memory assetData, + uint256 amount + ) + internal + { + bytes4 proxyId = assetData.readBytes4(0); + + if (proxyId == ERC20_DATA_ID) { + transferERC20Token(assetData, amount); + } else if (proxyId == ERC721_DATA_ID) { + transferERC721Token(assetData, amount); + } else { + revert("UNSUPPORTED_TOKEN_PROXY"); + } + } + + /// @dev Decodes ERC20 assetData and transfers given amount to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferERC20Token( + bytes memory assetData, + uint256 amount + ) + internal + { + address token = assetData.readAddress(16); + + // Transfer tokens. + // We do a raw call so we can check the success separate + // from the return data. + bool success = token.call(abi.encodeWithSelector( + ERC20_TRANSFER_SELECTOR, + msg.sender, + amount + )); + require( + success, + "TRANSFER_FAILED" + ); + + // 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 + // value that evaluates to true. + assembly { + if returndatasize { + success := 0 + if eq(returndatasize, 32) { + // First 64 bytes of memory are reserved scratch space + returndatacopy(0, 0, 32) + success := mload(0) + } + } + } + require( + success, + "TRANSFER_FAILED" + ); + } + + /// @dev Decodes ERC721 assetData and transfers given amount to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferERC721Token( + bytes memory assetData, + uint256 amount + ) + internal + { + require( + amount == 1, + "INVALID_AMOUNT" + ); + // Decode asset data. + address token = assetData.readAddress(16); + uint256 tokenId = assetData.readUint256(36); + + // Perform transfer. + IERC721Token(token).transferFrom( + address(this), + msg.sender, + tokenId + ); + } +} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol new file mode 100644 index 000000000..4584bb840 --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/MixinExchangeWrapper.sol @@ -0,0 +1,263 @@ +/* + + 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 "./libs/LibConstants.sol"; +import "./mixins/MExchangeWrapper.sol"; +import "../protocol/Exchange/libs/LibAbiEncoder.sol"; +import "../protocol/Exchange/libs/LibOrder.sol"; +import "../protocol/Exchange/libs/LibFillResults.sol"; +import "../protocol/Exchange/libs/LibMath.sol"; + + +contract MixinExchangeWrapper is + LibAbiEncoder, + LibFillResults, + LibMath, + LibConstants, + MExchangeWrapper +{ + + /// @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 + ) + internal + returns (FillResults memory fillResults) + { + // ABI encode calldata for `fillOrder` + bytes memory fillOrderCalldata = abiEncodeFillOrder( + order, + takerAssetFillAmount, + signature + ); + + address exchange = address(EXCHANGE); + + // Call `fillOrder` and handle any exceptions gracefully + assembly { + let success := call( + gas, // forward all gas, TODO: look into gas consumption of assert/throw + exchange, // call address of Exchange contract + 0, // transfer 0 wei + 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 + ) + switch success + case 0 { + mstore(fillResults, 0) + mstore(add(fillResults, 32), 0) + mstore(add(fillResults, 64), 0) + mstore(add(fillResults, 96), 0) + } + case 1 { + 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))) + } + } + return fillResults; + } + + /// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH has been sold by taker. + /// Returns false if the transaction would otherwise revert. + /// @param orders Array of order specifications. + /// @param wethSellAmount Desired amount of WETH to sell. + /// @param signatures Proofs that orders have been signed by makers. + /// @return Amounts filled and fees paid by makers and taker. + function marketSellWeth( + LibOrder.Order[] memory orders, + uint256 wethSellAmount, + bytes[] memory signatures + ) + internal + returns (FillResults memory totalFillResults) + { + bytes memory makerAssetData = orders[0].makerAssetData; + bytes memory wethAssetData = WETH_ASSET_DATA; + + 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. + // We assume that asset being sold by taker is WETH for each order. + orders[i].makerAssetData = makerAssetData; + orders[i].takerAssetData = wethAssetData; + + // Calculate the remaining amount of WETH to sell + uint256 remainingTakerAssetFillAmount = safeSub(wethSellAmount, totalFillResults.takerAssetFilledAmount); + + // Attempt to sell the remaining amount of WETH + 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 >= wethSellAmount) { + 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. + /// The asset being sold by taker must always be WETH. + /// @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 marketBuyExactAmountWithWeth( + LibOrder.Order[] memory orders, + uint256 makerAssetFillAmount, + bytes[] memory signatures + ) + internal + returns (FillResults memory totalFillResults) + { + bytes memory makerAssetData = orders[0].makerAssetData; + bytes memory wethAssetData = WETH_ASSET_DATA; + + 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. + // We assume that asset being sold by taker is WETH for each order. + orders[i].makerAssetData = makerAssetData; + orders[i].takerAssetData = wethAssetData; + + // 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 = getPartialAmount( + 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 + uint256 makerAssetFilledAmount = totalFillResults.makerAssetFilledAmount; + if (makerAssetFilledAmount >= makerAssetFillAmount) { + break; + } + } + + require( + makerAssetFilledAmount >= makerAssetFillAmount, + "COMPLETE_FILL_FAILED" + ); + return totalFillResults; + } + + /// @dev Buys zrxBuyAmount of ZRX fee tokens, taking into account ZRX fees for each order. This will guarantee + /// that at least zrxBuyAmount of ZRX is purchased (sometimes slightly over due to rounding issues). + /// It is possible that a request to buy 200 ZRX will require purchasing 202 ZRX + /// as 2 ZRX is required to purchase the 200 ZRX fee tokens. This guarantees at least 200 ZRX for future purchases. + /// The asset being sold by taker must always be WETH. + /// @param orders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. + /// @param zrxBuyAmount Desired amount of ZRX to buy. + /// @param signatures Proofs that orders have been created by makers. + /// @return totalFillResults Amounts filled and fees paid by maker and taker. + function marketBuyExactZrxWithWeth( + LibOrder.Order[] memory orders, + uint256 zrxBuyAmount, + bytes[] memory signatures + ) + internal + returns (FillResults memory totalFillResults) + { + // Do nothing if zrxBuyAmount == 0 + if (zrxBuyAmount == 0) { + return totalFillResults; + } + + bytes memory zrxAssetData = ZRX_ASSET_DATA; + bytes memory wethAssetData = WETH_ASSET_DATA; + uint256 zrxPurchased = 0; + + uint256 ordersLength = orders.length; + for (uint256 i = 0; i != ordersLength; i++) { + + // All of these are ZRX/WETH, so we can drop the respective assetData from calldata. + orders[i].makerAssetData = zrxAssetData; + orders[i].takerAssetData = wethAssetData; + + // Calculate the remaining amount of ZRX to buy. + uint256 remainingZrxBuyAmount = safeSub(zrxBuyAmount, zrxPurchased); + + // Convert the remaining amount of ZRX to buy into remaining amount + // of WETH to sell, assuming entire amount can be sold in the current order. + uint256 remainingWethSellAmount = getPartialAmount( + orders[i].takerAssetAmount, + safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees + remainingZrxBuyAmount + ); + + // Attempt to sell the remaining amount of WETH. + FillResults memory singleFillResult = fillOrderNoThrow( + orders[i], + safeAdd(remainingWethSellAmount, 1), // we add 1 wei to the fill amount to make up for rounding errors + signatures[i] + ); + + // Update amounts filled and fees paid by maker and taker. + addFillResults(totalFillResults, singleFillResult); + zrxPurchased = safeSub(totalFillResults.makerAssetFilledAmount, totalFillResults.takerFeePaid); + + // Stop execution if the entire amount of ZRX has been bought. + if (zrxPurchased >= zrxBuyAmount) { + break; + } + } + + require( + zrxPurchased >= zrxBuyAmount, + "COMPLETE_FILL_FAILED" + ); + return totalFillResults; + } +} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol new file mode 100644 index 000000000..93cbf79be --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/MixinForwarderCore.sol @@ -0,0 +1,213 @@ +/* + + 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 "./libs/LibConstants.sol"; +import "./mixins/MWeth.sol"; +import "./mixins/MAssets.sol"; +import "./mixins/MExchangeWrapper.sol"; +import "./interfaces/IForwarderCore.sol"; +import "../utils/LibBytes/LibBytes.sol"; +import "../protocol/Exchange/libs/LibOrder.sol"; +import "../protocol/Exchange/libs/LibFillResults.sol"; +import "../protocol/Exchange/libs/LibMath.sol"; + + +contract MixinForwarderCore is + LibFillResults, + LibMath, + LibConstants, + MWeth, + MAssets, + MExchangeWrapper, + IForwarderCore +{ + + using LibBytes for bytes; + + /// @dev Constructor approves ERC20 proxy to transfer ZRX and WETH on this contract's behalf. + constructor () + public + { + address proxyAddress = EXCHANGE.getAssetProxy(ERC20_DATA_ID); + if (proxyAddress != address(0)) { + ETHER_TOKEN.approve(proxyAddress, MAX_UINT); + ZRX_TOKEN.approve(proxyAddress, MAX_UINT); + } + } + + /// @dev Purchases as much of orders' makerAssets as possible by selling up to 95% of transaction's ETH value. + /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. + /// 5% of ETH value is reserved for paying fees to order feeRecipients (in ZRX) and forwarding contract feeRecipient (in ETH). + /// Any ETH not spent will be refunded to sender. + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param signatures Proofs that orders have been created by makers. + /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. + /// @param feeSignatures Proofs that feeOrders have been created by makers. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + /// @return Amounts filled and fees paid by maker and taker for both sets of orders. + function marketSellOrdersWithEth( + LibOrder.Order[] memory orders, + bytes[] memory signatures, + LibOrder.Order[] memory feeOrders, + bytes[] memory feeSignatures, + uint256 feePercentage, + address feeRecipient + ) + public + payable + returns ( + FillResults memory orderFillResults, + FillResults memory feeOrderFillResults + ) + { + // Convert ETH to WETH. + convertEthToWeth(); + + uint256 wethSellAmount; + uint256 zrxBuyAmount; + uint256 makerAssetAmountPurchased; + if (orders[0].makerAssetData.equals(ZRX_ASSET_DATA)) { + // Calculate amount of WETH that won't be spent on ETH fees. + wethSellAmount = getPartialAmount( + PERCENTAGE_DENOMINATOR, + safeAdd(PERCENTAGE_DENOMINATOR, feePercentage), + msg.value + ); + // Market sell available WETH. + // ZRX fees are paid with this contract's balance. + orderFillResults = marketSellWeth( + orders, + wethSellAmount, + signatures + ); + // The fee amount must be deducted from the amount transfered back to sender. + makerAssetAmountPurchased = safeSub(orderFillResults.makerAssetFilledAmount, orderFillResults.takerFeePaid); + } else { + // 5% of WETH is reserved for filling feeOrders and paying feeRecipient. + wethSellAmount = getPartialAmount( + MAX_WETH_FILL_PERCENTAGE, + PERCENTAGE_DENOMINATOR, + msg.value + ); + // Market sell 95% of WETH. + // ZRX fees are payed with this contract's balance. + orderFillResults = marketSellWeth( + orders, + wethSellAmount, + signatures + ); + // Buy back all ZRX spent on fees. + zrxBuyAmount = orderFillResults.takerFeePaid; + feeOrderFillResults = marketBuyExactZrxWithWeth( + feeOrders, + zrxBuyAmount, + feeSignatures + ); + makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount; + } + + // Transfer feePercentage of total ETH spent on primary orders to feeRecipient. + // Refund remaining ETH to msg.sender. + transferEthFeeAndRefund( + orderFillResults.takerAssetFilledAmount, + feeOrderFillResults.takerAssetFilledAmount, + feePercentage, + feeRecipient + ); + + // Transfer purchased assets to msg.sender. + transferAssetToSender(orders[0].makerAssetData, makerAssetAmountPurchased); + } + + /// @dev Attempt to purchase makerAssetFillAmount of makerAsset by selling ETH provided with transaction. + /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. + /// Any ETH not spent will be refunded to sender. + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param makerAssetFillAmount Desired amount of makerAsset to purchase. + /// @param signatures Proofs that orders have been created by makers. + /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. + /// @param feeSignatures Proofs that feeOrders have been created by makers. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + /// @return Amounts filled and fees paid by maker and taker for both sets of orders. + function marketBuyOrdersWithEth( + LibOrder.Order[] memory orders, + uint256 makerAssetFillAmount, + bytes[] memory signatures, + LibOrder.Order[] memory feeOrders, + bytes[] memory feeSignatures, + uint256 feePercentage, + address feeRecipient + ) + public + payable + returns ( + FillResults memory orderFillResults, + FillResults memory feeOrderFillResults + ) + { + // Convert ETH to WETH. + convertEthToWeth(); + + uint256 zrxBuyAmount; + uint256 makerAssetAmountPurchased; + if (orders[0].makerAssetData.equals(ZRX_ASSET_DATA)) { + // If the makerAsset is ZRX, it is not necessary to pay fees out of this + // contracts's ZRX balance because fees are factored into the price of the order. + orderFillResults = marketBuyExactZrxWithWeth( + orders, + makerAssetFillAmount, + signatures + ); + // The fee amount must be deducted from the amount transfered back to sender. + makerAssetAmountPurchased = safeSub(orderFillResults.makerAssetFilledAmount, orderFillResults.takerFeePaid); + } else { + // Attemp to purchase desired amount of makerAsset. + // ZRX fees are payed with this contract's balance. + orderFillResults = marketBuyExactAmountWithWeth( + orders, + makerAssetFillAmount, + signatures + ); + // Buy back all ZRX spent on fees. + zrxBuyAmount = orderFillResults.takerFeePaid; + feeOrderFillResults = marketBuyExactZrxWithWeth( + feeOrders, + zrxBuyAmount, + feeSignatures + ); + makerAssetAmountPurchased = orderFillResults.makerAssetFilledAmount; + } + + // Transfer feePercentage of total ETH spent on primary orders to feeRecipient. + // Refund remaining ETH to msg.sender. + transferEthFeeAndRefund( + orderFillResults.takerAssetFilledAmount, + feeOrderFillResults.takerAssetFilledAmount, + feePercentage, + feeRecipient + ); + + // Transfer purchased assets to msg.sender. + transferAssetToSender(orders[0].makerAssetData, makerAssetAmountPurchased); + } +} diff --git a/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol new file mode 100644 index 000000000..e07940776 --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/MixinWeth.sol @@ -0,0 +1,114 @@ +/* + + 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/libs/LibMath.sol"; +import "./libs/LibConstants.sol"; +import "./mixins/MWeth.sol"; + + +contract MixinWeth is + LibMath, + LibConstants, + MWeth +{ + + /// @dev Default payabale function, this allows us to withdraw WETH + function () + public + payable + { + require( + msg.sender == address(ETHER_TOKEN), + "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY" + ); + } + + /// @dev Converts message call's ETH value into WETH. + function convertEthToWeth() + internal + { + require( + msg.value > 0, + "INVALID_MSG_VALUE" + ); + ETHER_TOKEN.deposit.value(msg.value)(); + } + + /// @dev Transfers feePercentage of WETH spent on primary orders to feeRecipient. + /// Refunds any excess ETH to msg.sender. + /// @param wethSoldExcludingFeeOrders Amount of WETH sold when filling primary orders. + /// @param wethSoldForZrx Amount of WETH sold when purchasing ZRX required for primary order fees. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + function transferEthFeeAndRefund( + uint256 wethSoldExcludingFeeOrders, + uint256 wethSoldForZrx, + uint256 feePercentage, + address feeRecipient + ) + internal + { + // Ensure feePercentage is less than 5%. + require( + feePercentage <= MAX_FEE_PERCENTAGE, + "FEE_PERCENTAGE_TOO_LARGE" + ); + + // Ensure that no extra WETH owned by this contract has been sold. + uint256 wethSold = safeAdd(wethSoldExcludingFeeOrders, wethSoldForZrx); + require( + wethSold <= msg.value, + "OVERSOLD_WETH" + ); + + // Calculate amount of WETH that hasn't been sold. + uint256 wethRemaining = safeSub(msg.value, wethSold); + + // Calculate ETH fee to pay to feeRecipient. + uint256 ethFee = getPartialAmount( + feePercentage, + PERCENTAGE_DENOMINATOR, + wethSoldExcludingFeeOrders + ); + + // Ensure fee is less than amount of WETH remaining. + require( + ethFee <= wethRemaining, + "INSUFFICIENT_ETH_REMAINING" + ); + + // Do nothing if no WETH remaining + if (wethRemaining > 0) { + // Convert remaining WETH to ETH + ETHER_TOKEN.withdraw(wethRemaining); + + // Pay ETH to feeRecipient + if (ethFee > 0) { + feeRecipient.transfer(ethFee); + } + + // Refund remaining ETH to msg.sender. + uint256 ethRefund = safeSub(wethRemaining, ethFee); + if (ethRefund > 0) { + msg.sender.transfer(ethRefund); + } + } + } +} diff --git a/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol new file mode 100644 index 000000000..1e034c003 --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/interfaces/IAssets.sol @@ -0,0 +1,34 @@ +/* + + 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; + + +contract IAssets { + + /// @dev Withdraws assets from this contract. The contract requires a ZRX balance in order to + /// function optimally, and this function allows the ZRX to be withdrawn by owner. It may also be + /// used to withdraw assets that were accidentally sent to this contract. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of ERC20 token to withdraw. + function withdrawAsset( + bytes assetData, + uint256 amount + ) + external; +} diff --git a/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol new file mode 100644 index 000000000..f5a26e2ba --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarder.sol @@ -0,0 +1,30 @@ +/* + + 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 "./IForwarderCore.sol"; +import "./IAssets.sol"; + + +// solhint-disable no-empty-blocks +contract IForwarder is + IForwarderCore, + IAssets +{} diff --git a/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol b/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol new file mode 100644 index 000000000..3ecbb133b --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/interfaces/IForwarderCore.sol @@ -0,0 +1,80 @@ +/* + + 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/libs/LibOrder.sol"; +import "../../protocol/Exchange/libs/LibFillResults.sol"; + + +contract IForwarderCore { + + /// @dev Purchases as much of orders' makerAssets as possible by selling up to 95% of transaction's ETH value. + /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. + /// 5% of ETH value is reserved for paying fees to order feeRecipients (in ZRX) and forwarding contract feeRecipient (in ETH). + /// Any ETH not spent will be refunded to sender. + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param signatures Proofs that orders have been created by makers. + /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. + /// @param feeSignatures Proofs that feeOrders have been created by makers. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + /// @return Amounts filled and fees paid by maker and taker for both sets of orders. + function marketSellOrdersWithEth( + LibOrder.Order[] memory orders, + bytes[] memory signatures, + LibOrder.Order[] memory feeOrders, + bytes[] memory feeSignatures, + uint256 feePercentage, + address feeRecipient + ) + public + payable + returns ( + LibFillResults.FillResults memory orderFillResults, + LibFillResults.FillResults memory feeOrderFillResults + ); + + /// @dev Attempt to purchase makerAssetFillAmount of makerAsset by selling ETH provided with transaction. + /// Any ZRX required to pay fees for primary orders will automatically be purchased by this contract. + /// Any ETH not spent will be refunded to sender. + /// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset. + /// @param makerAssetFillAmount Desired amount of makerAsset to purchase. + /// @param signatures Proofs that orders have been created by makers. + /// @param feeOrders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. Used to purchase ZRX for primary order fees. + /// @param feeSignatures Proofs that feeOrders have been created by makers. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + /// @return Amounts filled and fees paid by maker and taker for both sets of orders. + function marketBuyOrdersWithEth( + LibOrder.Order[] memory orders, + uint256 makerAssetFillAmount, + bytes[] memory signatures, + LibOrder.Order[] memory feeOrders, + bytes[] memory feeSignatures, + uint256 feePercentage, + address feeRecipient + ) + public + payable + returns ( + LibFillResults.FillResults memory orderFillResults, + LibFillResults.FillResults memory feeOrderFillResults + ); +} diff --git a/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol b/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.sol new file mode 100644 index 000000000..c26d7902c --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/libs/LibConstants.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/Exchange/interfaces/IExchange.sol"; +import "../../tokens/EtherToken/IEtherToken.sol"; +import "../../tokens/ERC20Token/IERC20Token.sol"; + + +contract LibConstants { + + bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); + bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256)")); + uint256 constant internal MAX_UINT = 2**256 - 1; + uint256 constant internal PERCENTAGE_DENOMINATOR = 10**18; + uint256 constant internal MAX_FEE_PERCENTAGE = 5 * PERCENTAGE_DENOMINATOR / 100; // 5% + uint256 constant internal MAX_WETH_FILL_PERCENTAGE = 95 * PERCENTAGE_DENOMINATOR / 100; // 95% + + // solhint-disable var-name-mixedcase + IExchange internal EXCHANGE; + IEtherToken internal ETHER_TOKEN; + IERC20Token internal ZRX_TOKEN; + bytes internal ZRX_ASSET_DATA; + bytes internal WETH_ASSET_DATA; + // solhint-enable var-name-mixedcase + + constructor ( + address _exchange, + address _etherToken, + address _zrxToken, + bytes memory _zrxAssetData, + bytes memory _wethAssetData + ) + public + { + EXCHANGE = IExchange(_exchange); + ETHER_TOKEN = IEtherToken(_etherToken); + ZRX_TOKEN = IERC20Token(_zrxToken); + ZRX_ASSET_DATA = _zrxAssetData; + WETH_ASSET_DATA = _wethAssetData; + } +} diff --git a/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol b/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol new file mode 100644 index 000000000..cdfb77a0b --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/libs/LibForwarderErrors.sol @@ -0,0 +1,34 @@ +/* + + 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. + +*/ + +// solhint-disable +pragma solidity 0.4.24; + + +/// This contract is intended to serve as a reference, but is not actually used for efficiency reasons. +contract LibForwarderErrors { + string constant FEE_PERCENTAGE_TOO_LARGE = "FEE_PROPORTION_TOO_LARGE"; // Provided fee percentage greater than 5%. + string constant INSUFFICIENT_ETH_REMAINING = "INSUFFICIENT_ETH_REMAINING"; // Not enough ETH remaining to pay feeRecipient. + string constant OVERSOLD_WETH = "OVERSOLD_WETH"; // More WETH sold than provided with current message call. + string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired purchase amount not completely filled (required for ZRX fees only). + string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Asset transfer failed. + string constant UNSUPPORTED_TOKEN_PROXY = "UNSUPPORTED_TOKEN_PROXY"; // Proxy in assetData not supported. + string constant DEFAULT_FUNCTION_WETH_CONTRACT_ONLY = "DEFAULT_FUNCTION_WETH_CONTRACT_ONLY"; // Fallback function may only be used for WETH withdrawals. + string constant INVALID_MSG_VALUE = "INVALID_MSG_VALUE"; // msg.value must be greater than 0. + string constant INVALID_AMOUNT = "INVALID_AMOUNT"; // Amount must equal 1. +} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol new file mode 100644 index 000000000..83636432a --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MAssets.sol @@ -0,0 +1,54 @@ +/* + + 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 "../interfaces/IAssets.sol"; + + +contract MAssets is + IAssets +{ + + /// @dev Transfers given amount of asset to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferAssetToSender( + bytes memory assetData, + uint256 amount + ) + internal; + + /// @dev Decodes ERC20 assetData and transfers given amount to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferERC20Token( + bytes memory assetData, + uint256 amount + ) + internal; + + /// @dev Decodes ERC721 assetData and transfers given amount to sender. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param amount Amount of asset to transfer to sender. + function transferERC721Token( + bytes memory assetData, + uint256 amount + ) + internal; +} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol new file mode 100644 index 000000000..360dea0e4 --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MExchangeWrapper.sol @@ -0,0 +1,87 @@ +/* + + 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/libs/LibOrder.sol"; +import "../../protocol/Exchange/libs/LibFillResults.sol"; + + +contract MExchangeWrapper { + + /// @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 + ) + internal + returns (LibFillResults.FillResults memory fillResults); + + /// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH has been sold by taker. + /// Returns false if the transaction would otherwise revert. + /// @param orders Array of order specifications. + /// @param wethSellAmount Desired amount of WETH to sell. + /// @param signatures Proofs that orders have been signed by makers. + /// @return Amounts filled and fees paid by makers and taker. + function marketSellWeth( + LibOrder.Order[] memory orders, + uint256 wethSellAmount, + bytes[] memory signatures + ) + internal + returns (LibFillResults.FillResults memory 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. + /// The asset being sold by taker must always be WETH. + /// @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 marketBuyExactAmountWithWeth( + LibOrder.Order[] memory orders, + uint256 makerAssetFillAmount, + bytes[] memory signatures + ) + internal + returns (LibFillResults.FillResults memory totalFillResults); + + /// @dev Buys zrxBuyAmount of ZRX fee tokens, taking into account ZRX fees for each order. This will guarantee + /// that at least zrxBuyAmount of ZRX is purchased (sometimes slightly over due to rounding issues). + /// It is possible that a request to buy 200 ZRX will require purchasing 202 ZRX + /// as 2 ZRX is required to purchase the 200 ZRX fee tokens. This guarantees at least 200 ZRX for future purchases. + /// The asset being sold by taker must always be WETH. + /// @param orders Array of order specifications containing ZRX as makerAsset and WETH as takerAsset. + /// @param zrxBuyAmount Desired amount of ZRX to buy. + /// @param signatures Proofs that orders have been created by makers. + /// @return totalFillResults Amounts filled and fees paid by maker and taker. + function marketBuyExactZrxWithWeth( + LibOrder.Order[] memory orders, + uint256 zrxBuyAmount, + bytes[] memory signatures + ) + internal + returns (LibFillResults.FillResults memory totalFillResults); +} diff --git a/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol b/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.sol new file mode 100644 index 000000000..88e77be4e --- /dev/null +++ b/packages/contracts/src/2.0.0/forwarder/mixins/MWeth.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; + + +contract MWeth { + + /// @dev Converts message call's ETH value into WETH. + function convertEthToWeth() + internal; + + /// @dev Transfers feePercentage of WETH spent on primary orders to feeRecipient. + /// Refunds any excess ETH to msg.sender. + /// @param wethSoldExcludingFeeOrders Amount of WETH sold when filling primary orders. + /// @param wethSoldForZrx Amount of WETH sold when purchasing ZRX required for primary order fees. + /// @param feePercentage Percentage of WETH sold that will payed as fee to forwarding contract feeRecipient. + /// @param feeRecipient Address that will receive ETH when orders are filled. + function transferEthFeeAndRefund( + uint256 wethSoldExcludingFeeOrders, + uint256 wethSoldForZrx, + uint256 feePercentage, + address feeRecipient + ) + internal; +} diff --git a/packages/contracts/src/contracts/current/multisig/MultiSigWallet/MultiSigWallet.sol b/packages/contracts/src/2.0.0/multisig/MultiSigWallet.sol index 79fd92029..eb54fe047 100644 --- a/packages/contracts/src/contracts/current/multisig/MultiSigWallet/MultiSigWallet.sol +++ b/packages/contracts/src/2.0.0/multisig/MultiSigWallet.sol @@ -1,5 +1,7 @@ +// solhint-disable pragma solidity ^0.4.10; + /// @title Multisignature wallet - Allows multiple parties to agree on transactions before execution. /// @author Stefan George - <stefan.george@consensys.net> contract MultiSigWallet { diff --git a/packages/contracts/src/contracts/current/multisig/MultiSigWalletWithTimeLock/MultiSigWalletWithTimeLock.sol b/packages/contracts/src/2.0.0/multisig/MultiSigWalletWithTimeLock.sol index a545d9813..8c5e6e1e6 100644 --- a/packages/contracts/src/contracts/current/multisig/MultiSigWalletWithTimeLock/MultiSigWalletWithTimeLock.sol +++ b/packages/contracts/src/2.0.0/multisig/MultiSigWalletWithTimeLock.sol @@ -1,6 +1,6 @@ /* - Copyright 2017 ZeroEx Intl. + 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. @@ -16,9 +16,11 @@ */ +// solhint-disable pragma solidity ^0.4.10; -import { MultiSigWallet } from "../MultiSigWallet/MultiSigWallet.sol"; +import "./MultiSigWallet.sol"; + /// @title Multisignature wallet with time lock- Allows multiple parties to execute a transaction after a time lock has passed. /// @author Amir Bandeali - <amir@0xProject.com> diff --git a/packages/contracts/src/2.0.0/protocol/AssetProxy/ERC20Proxy.sol b/packages/contracts/src/2.0.0/protocol/AssetProxy/ERC20Proxy.sol new file mode 100644 index 000000000..b5cec6b64 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/AssetProxy/ERC20Proxy.sol @@ -0,0 +1,133 @@ +/* + + 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 "../../utils/LibBytes/LibBytes.sol"; +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) + } + + /////// Token contract address /////// + // The token address is found as follows: + // * It is stored at offset 4 in `assetData` contents. + // * This is stored at offset 32 from `assetData`. + // * The offset to `assetData` from Params is stored at offset + // 4 in calldata. + // * The offset of Params in calldata is 4. + // So we read location 4 and add 32 + 4 + 4 to it. + 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) + } + } + } + + /// @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/packages/contracts/src/2.0.0/protocol/AssetProxy/ERC721Proxy.sol b/packages/contracts/src/2.0.0/protocol/AssetProxy/ERC721Proxy.sol new file mode 100644 index 000000000..6a70c9f60 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/AssetProxy/ERC721Proxy.sol @@ -0,0 +1,167 @@ +/* + + 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 "../../utils/LibBytes/LibBytes.sol"; +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. + // + // 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) + } + } + } + + /// @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/packages/contracts/src/2.0.0/protocol/AssetProxy/MixinAuthorizable.sol b/packages/contracts/src/2.0.0/protocol/AssetProxy/MixinAuthorizable.sol new file mode 100644 index 000000000..ff4660a31 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/AssetProxy/MixinAuthorizable.sol @@ -0,0 +1,118 @@ +/* + + 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 "../../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/packages/contracts/src/2.0.0/protocol/AssetProxy/interfaces/IAssetData.sol b/packages/contracts/src/2.0.0/protocol/AssetProxy/interfaces/IAssetData.sol new file mode 100644 index 000000000..3e76e38dd --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/AssetProxy/interfaces/IAssetData.sol @@ -0,0 +1,40 @@ +/* + + 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. + +*/ + +// solhint-disable +pragma solidity 0.4.24; + + +// @dev Interface of the asset proxy's assetData. +// The asset proxies take an ABI encoded `bytes assetData` as argument. +// This argument is ABI encoded as one of the methods of this interface. +interface IAssetData { + + function ERC20Token(address tokenContract) + external + pure; + + function ERC721Token( + address tokenContract, + uint256 tokenId, + bytes receiverData + ) + external + pure; + +} diff --git a/packages/contracts/src/2.0.0/protocol/AssetProxy/interfaces/IAssetProxy.sol b/packages/contracts/src/2.0.0/protocol/AssetProxy/interfaces/IAssetProxy.sol new file mode 100644 index 000000000..3651dd694 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/AssetProxy/interfaces/IAssetProxy.sol @@ -0,0 +1,47 @@ +/* + + 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 "./IAuthorizable.sol"; + + +contract IAssetProxy is + IAuthorizable +{ + + /// @dev Transfers assets. Either succeeds or throws. + /// @param assetData Byte array encoded for the respective asset proxy. + /// @param from Address to transfer asset from. + /// @param to Address to transfer asset to. + /// @param amount Amount of asset to transfer. + function transferFrom( + bytes assetData, + address from, + address to, + uint256 amount + ) + external; + + /// @dev Gets the proxy id associated with the proxy address. + /// @return Proxy id. + function getProxyId() + external + pure + returns (bytes4); +} diff --git a/packages/contracts/src/2.0.0/protocol/AssetProxy/interfaces/IAuthorizable.sol b/packages/contracts/src/2.0.0/protocol/AssetProxy/interfaces/IAuthorizable.sol new file mode 100644 index 000000000..8fac43a47 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/AssetProxy/interfaces/IAuthorizable.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; + +import "../../../utils/Ownable/IOwnable.sol"; + + +contract IAuthorizable is + IOwnable +{ + + /// @dev Authorizes an address. + /// @param target Address to authorize. + function addAuthorizedAddress(address target) + external; + + /// @dev Removes authorizion of an address. + /// @param target Address to remove authorization from. + function removeAuthorizedAddress(address target) + external; + + /// @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; + + /// @dev Gets all authorized addresses. + /// @return Array of authorized addresses. + function getAuthorizedAddresses() + external + view + returns (address[] memory); +} diff --git a/packages/contracts/src/2.0.0/protocol/AssetProxy/libs/LibAssetProxyErrors.sol b/packages/contracts/src/2.0.0/protocol/AssetProxy/libs/LibAssetProxyErrors.sol new file mode 100644 index 000000000..1d9a70cc1 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/AssetProxy/libs/LibAssetProxyErrors.sol @@ -0,0 +1,38 @@ +/* + + 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. + +*/ + +// solhint-disable +pragma solidity 0.4.24; + + +/// @dev This contract documents the revert reasons used in the AssetProxy contracts. +/// This contract is intended to serve as a reference, but is not actually used for efficiency reasons. +contract LibAssetProxyErrors { + + /// Authorizable errors /// + string constant SENDER_NOT_AUTHORIZED = "SENDER_NOT_AUTHORIZED"; // Sender not authorized to call this method. + string constant TARGET_NOT_AUTHORIZED = "TARGET_NOT_AUTHORIZED"; // Target address not authorized to call this method. + string constant TARGET_ALREADY_AUTHORIZED = "TARGET_ALREADY_AUTHORIZED"; // Target address must not already be authorized. + string constant INDEX_OUT_OF_BOUNDS = "INDEX_OUT_OF_BOUNDS"; // Specified array index is out of bounds. + string constant AUTHORIZED_ADDRESS_MISMATCH = "AUTHORIZED_ADDRESS_MISMATCH"; // Address at index does not match given target address. + + /// Transfer errors /// + string constant INVALID_AMOUNT = "INVALID_AMOUNT"; // Transfer amount must equal 1. + string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Transfer failed. + string constant LENGTH_GREATER_THAN_131_REQUIRED = "LENGTH_GREATER_THAN_131_REQUIRED"; // Byte array must have a length greater than 0. +} diff --git a/packages/contracts/src/2.0.0/protocol/AssetProxy/mixins/MAuthorizable.sol b/packages/contracts/src/2.0.0/protocol/AssetProxy/mixins/MAuthorizable.sol new file mode 100644 index 000000000..8afc8c8d8 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/AssetProxy/mixins/MAuthorizable.sol @@ -0,0 +1,42 @@ +/* + + 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 "../interfaces/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/packages/contracts/src/2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol b/packages/contracts/src/2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol new file mode 100644 index 000000000..8b7333646 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol @@ -0,0 +1,119 @@ +/* + + 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.10; + +import "../../multisig/MultiSigWalletWithTimeLock.sol"; + + +contract AssetProxyOwner is + MultiSigWalletWithTimeLock +{ + + 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 tx = transactions[transactionId]; + require(isAssetProxyRegistered[tx.destination]); + require(readBytes4(tx.data, 0) == REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_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. + function AssetProxyOwner( + 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)); + 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; + 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 tx = transactions[transactionId]; + tx.executed = true; + // solhint-disable-next-line avoid-call-value + if (tx.destination.call.value(tx.value)(tx.data)) + Execution(transactionId); + else { + ExecutionFailure(transactionId); + tx.executed = false; + } + } + + /// @dev Reads an unpadded bytes4 value from a position in a byte array. + /// @param b Byte array containing a bytes4 value. + /// @param index Index in byte array of bytes4 value. + /// @return bytes4 value from byte array. + function readBytes4( + bytes memory b, + uint256 index + ) + internal + returns (bytes4 result) + { + require(b.length >= index + 4); + assembly { + result := mload(add(b, 32)) + // Solidity does not require us to clean the trailing bytes. + // We do it anyway + result := and(result, 0xFFFFFFFF00000000000000000000000000000000000000000000000000000000) + } + return result; + } +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/Exchange.sol b/packages/contracts/src/2.0.0/protocol/Exchange/Exchange.sol new file mode 100644 index 000000000..7507d3da1 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/Exchange.sol @@ -0,0 +1,54 @@ +/* + + 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 "./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/packages/contracts/src/2.0.0/protocol/Exchange/MixinAssetProxyDispatcher.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinAssetProxyDispatcher.sol new file mode 100644 index 000000000..e9f882194 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinAssetProxyDispatcher.sol @@ -0,0 +1,177 @@ +/* + + 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 "../../utils/Ownable/Ownable.sol"; +import "../../utils/LibBytes/LibBytes.sol"; +import "./mixins/MAssetProxyDispatcher.sol"; +import "../AssetProxy/interfaces/IAssetProxy.sol"; + + +contract MixinAssetProxyDispatcher is + Ownable, + MAssetProxyDispatcher +{ + using LibBytes for bytes; + + // 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) { + // Ensure assetData length is valid + require( + assetData.length > 3, + "LENGTH_GREATER_THAN_3_REQUIRED" + ); + + // Lookup assetProxy + 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/packages/contracts/src/2.0.0/protocol/Exchange/MixinExchangeCore.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinExchangeCore.sol new file mode 100644 index 000000000..ab5c6e507 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinExchangeCore.sol @@ -0,0 +1,435 @@ +/* + + 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 "./libs/LibConstants.sol"; +import "./libs/LibFillResults.sol"; +import "./libs/LibOrder.sol"; +import "./libs/LibMath.sol"; +import "./mixins/MExchangeCore.sol"; +import "./mixins/MSignatureValidator.sol"; +import "./mixins/MTransactions.sol"; +import "./mixins/MAssetProxyDispatcher.sol"; + + +contract MixinExchangeCore is + 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 + { + 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 + returns (FillResults memory fillResults) + { + // Fetch order info + OrderInfo memory orderInfo = getOrderInfo(order); + + // Fetch taker address + address takerAddress = getCurrentContextAddress(); + + // Get amount of takerAsset to fill + uint256 remainingTakerAssetAmount = safeSub(order.takerAssetAmount, orderInfo.orderTakerAssetFilledAmount); + uint256 takerAssetFilledAmount = min256(takerAssetFillAmount, remainingTakerAssetAmount); + + // Validate context + assertValidFill( + order, + orderInfo, + takerAddress, + takerAssetFillAmount, + takerAssetFilledAmount, + signature + ); + + // 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 cancelOrder(Order memory order) + public + { + // Fetch current order status + OrderInfo memory orderInfo = getOrderInfo(order); + + // Validate context + assertValidCancel(order, orderInfo); + + // Perform cancel + updateCancelledState(order, orderInfo.orderHash); + } + + /// @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 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 takerAssetFillAmount Desired amount of order to fill by taker. + /// @param takerAssetFilledAmount Amount of takerAsset that will be filled. + /// @param signature Proof that the orders was created by its maker. + function assertValidFill( + Order memory order, + OrderInfo memory orderInfo, + address takerAddress, + uint256 takerAssetFillAmount, + uint256 takerAssetFilledAmount, + bytes memory signature + ) + internal + view + { + // An order can only be filled if its status is FILLABLE. + require( + orderInfo.orderStatus == uint8(OrderStatus.FILLABLE), + "ORDER_UNFILLABLE" + ); + + // Revert if fill amount is invalid + require( + takerAssetFillAmount != 0, + "INVALID_TAKER_AMOUNT" + ); + + // 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" + ); + } + + // Validate fill order rounding + require( + !isRoundingError( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount + ), + "ROUNDING_ERROR" + ); + } + + /// @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 = getPartialAmount( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount + ); + fillResults.makerFeePaid = getPartialAmount( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerFee + ); + fillResults.takerFeePaid = getPartialAmount( + 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/packages/contracts/src/2.0.0/protocol/Exchange/MixinMatchOrders.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinMatchOrders.sol new file mode 100644 index 000000000..56b309a1b --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinMatchOrders.sol @@ -0,0 +1,299 @@ +/* + 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 "./libs/LibConstants.sol"; +import "./libs/LibMath.sol"; +import "./libs/LibOrder.sol"; +import "./libs/LibFillResults.sol"; +import "./mixins/MExchangeCore.sol"; +import "./mixins/MMatchOrders.sol"; +import "./mixins/MTransactions.sol"; +import "./mixins/MAssetProxyDispatcher.sol"; + + +contract MixinMatchOrders is + 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 + 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 + assertValidMatch(leftOrder, rightOrder); + + // Compute proportional fill amounts + matchedFillResults = calculateMatchedFillResults( + leftOrder, + rightOrder, + leftOrderInfo.orderTakerAssetFilledAmount, + rightOrderInfo.orderTakerAssetFilledAmount + ); + + // Validate fill contexts + assertValidFill( + leftOrder, + leftOrderInfo, + takerAddress, + matchedFillResults.left.takerAssetFilledAmount, + matchedFillResults.left.takerAssetFilledAmount, + leftSignature + ); + assertValidFill( + rightOrder, + rightOrderInfo, + takerAddress, + matchedFillResults.right.takerAssetFilledAmount, + matchedFillResults.right.takerAssetFilledAmount, + rightSignature + ); + + // Update exchange state + updateFilledState( + leftOrder, + takerAddress, + leftOrderInfo.orderHash, + leftOrderInfo.orderTakerAssetFilledAmount, + matchedFillResults.left + ); + updateFilledState( + rightOrder, + takerAddress, + rightOrderInfo.orderHash, + rightOrderInfo.orderTakerAssetFilledAmount, + matchedFillResults.right + ); + + // Settle matched orders. Succeeds or throws. + settleMatchedOrders( + leftOrder, + rightOrder, + takerAddress, + matchedFillResults + ); + + return matchedFillResults; + } + + /// @dev Validates context for matchOrders. Succeeds or throws. + /// @param leftOrder First order to match. + /// @param rightOrder Second order to match. + function assertValidMatch( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder + ) + internal + pure + { + // Make sure there is a profitable spread. + // There is a profitable spread iff the cost per unit bought (OrderA.MakerAmount/OrderA.TakerAmount) for each order is greater + // than the profit per unit sold of the matched order (OrderB.TakerAmount/OrderB.MakerAmount). + // This is satisfied by the equations below: + // <leftOrder.makerAssetAmount> / <leftOrder.takerAssetAmount> >= <rightOrder.takerAssetAmount> / <rightOrder.makerAssetAmount> + // AND + // <rightOrder.makerAssetAmount> / <rightOrder.takerAssetAmount> >= <leftOrder.takerAssetAmount> / <leftOrder.makerAssetAmount> + // These equations can be combined to get the following: + require( + safeMul(leftOrder.makerAssetAmount, rightOrder.makerAssetAmount) >= + safeMul(leftOrder.takerAssetAmount, rightOrder.takerAssetAmount), + "NEGATIVE_SPREAD_REQUIRED" + ); + } + + /// @dev Calculates fill amounts for the matched orders. + /// Each order is filled at their respective price point. However, the calculations are + /// carried out as though the orders are both being filled at the right order's price point. + /// The profit made by the leftOrder order goes to the taker (who matched the two orders). + /// @param leftOrder First order to match. + /// @param rightOrder Second order to match. + /// @param leftOrderTakerAssetFilledAmount Amount of left order already filled. + /// @param rightOrderTakerAssetFilledAmount Amount of right order already filled. + /// @param matchedFillResults Amounts to fill and fees to pay by maker and taker of matched orders. + function calculateMatchedFillResults( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + uint256 leftOrderTakerAssetFilledAmount, + uint256 rightOrderTakerAssetFilledAmount + ) + internal + pure + returns (LibFillResults.MatchedFillResults memory matchedFillResults) + { + // We settle orders at the exchange rate of the right order. + // The amount saved by the left maker goes to the taker. + // Either the left or right order will be fully filled; possibly both. + // The left order is fully filled iff the right order can sell more than left can buy. + // That is: the amount required to fill the left order is less than or equal to + // the amount we can spend from the right order: + // <leftTakerAssetAmountRemaining> <= <rightTakerAssetAmountRemaining> * <rightMakerToTakerRatio> + // <leftTakerAssetAmountRemaining> <= <rightTakerAssetAmountRemaining> * <rightOrder.makerAssetAmount> / <rightOrder.takerAssetAmount> + // <leftTakerAssetAmountRemaining> * <rightOrder.takerAssetAmount> <= <rightTakerAssetAmountRemaining> * <rightOrder.makerAssetAmount> + uint256 leftTakerAssetAmountRemaining = safeSub(leftOrder.takerAssetAmount, leftOrderTakerAssetFilledAmount); + uint256 rightTakerAssetAmountRemaining = safeSub(rightOrder.takerAssetAmount, rightOrderTakerAssetFilledAmount); + uint256 leftTakerAssetFilledAmount; + uint256 rightTakerAssetFilledAmount; + if ( + safeMul(leftTakerAssetAmountRemaining, rightOrder.takerAssetAmount) <= + safeMul(rightTakerAssetAmountRemaining, rightOrder.makerAssetAmount) + ) { + // Left order will be fully filled: maximally fill left + leftTakerAssetFilledAmount = leftTakerAssetAmountRemaining; + + // The right order receives an amount proportional to how much was spent. + rightTakerAssetFilledAmount = getPartialAmount( + rightOrder.takerAssetAmount, + rightOrder.makerAssetAmount, + leftTakerAssetFilledAmount + ); + } else { + // Right order will be fully filled: maximally fill right + rightTakerAssetFilledAmount = rightTakerAssetAmountRemaining; + + // The left order receives an amount proportional to how much was spent. + leftTakerAssetFilledAmount = getPartialAmount( + rightOrder.makerAssetAmount, + rightOrder.takerAssetAmount, + rightTakerAssetFilledAmount + ); + } + + // Calculate fill results for left order + matchedFillResults.left = calculateFillResults( + leftOrder, + leftTakerAssetFilledAmount + ); + + // Calculate fill results for right order + matchedFillResults.right = calculateFillResults( + rightOrder, + rightTakerAssetFilledAmount + ); + + // Calculate amount given to taker + matchedFillResults.leftMakerAssetSpreadAmount = safeSub( + matchedFillResults.left.makerAssetFilledAmount, + matchedFillResults.right.takerAssetFilledAmount + ); + + // 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/packages/contracts/src/2.0.0/protocol/Exchange/MixinSignatureValidator.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinSignatureValidator.sol new file mode 100644 index 000000000..44de54817 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinSignatureValidator.sol @@ -0,0 +1,260 @@ +/* + + 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 "../../utils/LibBytes/LibBytes.sol"; +import "./mixins/MSignatureValidator.sol"; +import "./mixins/MTransactions.sol"; +import "./interfaces/IWallet.sol"; +import "./interfaces/IValidator.sol"; + + +contract MixinSignatureValidator is + 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 + { + 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 + { + 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; + + // Implicitly signed by caller. + // The signer has initiated the call. In the case of non-contract + // accounts it means the transaction itself was signed. + // Example: let's say for a particular operation three signatures + // A, B and C are required. To submit the transaction, A and B can + // give a signature to C, who can then submit the transaction using + // `Caller` for his own signature. Or A and C can sign and B can + // submit using `Caller`. Having `Caller` allows this flexibility. + } else if (signatureType == SignatureType.Caller) { + require( + signature.length == 0, + "LENGTH_0_REQUIRED" + ); + isValid = signerAddress == msg.sender; + 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 = IWallet(signerAddress).isValidSignature(hash, 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 = IValidator(validatorAddress).isValidSignature( + hash, + signerAddress, + signature + ); + return isValid; + + // Signer signed hash previously using the preSign function. + } else if (signatureType == SignatureType.PreSigned) { + isValid = preSigned[hash][signerAddress]; + return isValid; + + // Signature from Trezor hardware wallet. + // It differs from web3.eth_sign in the encoding of message length + // (Bitcoin varint encoding vs ascii-decimal, the latter is not + // self-terminating which leads to ambiguities). + // See also: + // https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer + // https://github.com/trezor/trezor-mcu/blob/master/firmware/ethereum.c#L602 + // https://github.com/trezor/trezor-mcu/blob/master/firmware/crypto.c#L36 + } else if (signatureType == SignatureType.Trezor) { + 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:\n\x20", + hash + )), + v, + r, + s + ); + isValid = signerAddress == recovered; + 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"); + } +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinTransactions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinTransactions.sol new file mode 100644 index 000000000..821d30279 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinTransactions.sol @@ -0,0 +1,161 @@ +/* + + 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 "./libs/LibExchangeErrors.sol"; +import "./mixins/MSignatureValidator.sol"; +import "./mixins/MTransactions.sol"; +import "./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; + + // 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 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 contextAddress = currentContextAddress == address(0) ? msg.sender : currentContextAddress; + return contextAddress; + } +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol new file mode 100644 index 000000000..86194f461 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol @@ -0,0 +1,403 @@ +/* + + 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 "./libs/LibMath.sol"; +import "./libs/LibOrder.sol"; +import "./libs/LibFillResults.sol"; +import "./libs/LibAbiEncoder.sol"; +import "./mixins/MExchangeCore.sol"; + + +contract MixinWrapperFunctions is + LibMath, + LibFillResults, + LibAbiEncoder, + MExchangeCore +{ + + /// @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 + returns (FillResults memory fillResults) + { + fillResults = fillOrder( + order, + takerAssetFillAmount, + signature + ); + require( + fillResults.takerAssetFilledAmount == takerAssetFillAmount, + "COMPLETE_FILL_FAILED" + ); + 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, TODO: look into gas consumption of assert/throw + 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 + ) + switch success + case 0 { + mstore(fillResults, 0) + mstore(add(fillResults, 32), 0) + mstore(add(fillResults, 64), 0) + mstore(add(fillResults, 96), 0) + } + case 1 { + 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))) + } + } + 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 + returns (FillResults memory totalFillResults) + { + uint256 ordersLength = orders.length; + for (uint256 i = 0; i != ordersLength; i++) { + FillResults memory singleFillResults = fillOrder( + 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 + returns (FillResults memory totalFillResults) + { + uint256 ordersLength = orders.length; + for (uint256 i = 0; i != ordersLength; i++) { + FillResults memory singleFillResults = fillOrKillOrder( + 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 + 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 = fillOrder( + 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 + 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 = getPartialAmount( + orders[i].takerAssetAmount, + orders[i].makerAssetAmount, + remainingMakerAssetFillAmount + ); + + // Attempt to sell the remaining amount of takerAsset + FillResults memory singleFillResults = fillOrder( + 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 = getPartialAmount( + 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 + { + uint256 ordersLength = orders.length; + for (uint256 i = 0; i != ordersLength; i++) { + cancelOrder(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; + } +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol new file mode 100644 index 000000000..8db8d6f6c --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IAssetProxyDispatcher.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; + + +contract IAssetProxyDispatcher { + + /// @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; + + /// @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); +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IExchange.sol b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IExchange.sol new file mode 100644 index 000000000..b92abba04 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IExchange.sol @@ -0,0 +1,38 @@ +/* + + 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 "./IExchangeCore.sol"; +import "./IMatchOrders.sol"; +import "./ISignatureValidator.sol"; +import "./ITransactions.sol"; +import "./IAssetProxyDispatcher.sol"; +import "./IWrapperFunctions.sol"; + + +// solhint-disable no-empty-blocks +contract IExchange is + IExchangeCore, + IMatchOrders, + ISignatureValidator, + ITransactions, + IAssetProxyDispatcher, + IWrapperFunctions +{} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IExchangeCore.sol b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IExchangeCore.sol new file mode 100644 index 000000000..9995e0385 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IExchangeCore.sol @@ -0,0 +1,60 @@ +/* + + 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 "../libs/LibOrder.sol"; +import "../libs/LibFillResults.sol"; + + +contract IExchangeCore { + + /// @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; + + /// @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( + LibOrder.Order memory order, + uint256 takerAssetFillAmount, + bytes memory signature + ) + public + returns (LibFillResults.FillResults memory fillResults); + + /// @dev After calling, the order can not be filled anymore. + /// @param order Order struct containing order specifications. + function cancelOrder(LibOrder.Order memory order) + public; + + /// @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(LibOrder.Order memory order) + public + view + returns (LibOrder.OrderInfo memory orderInfo); +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IMatchOrders.sol b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IMatchOrders.sol new file mode 100644 index 000000000..73447f3ae --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IMatchOrders.sol @@ -0,0 +1,44 @@ +/* + + 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 "../libs/LibOrder.sol"; +import "../libs/LibFillResults.sol"; + + +contract IMatchOrders { + + /// @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 + returns (LibFillResults.MatchedFillResults memory matchedFillResults); +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/ISignatureValidator.sol b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/ISignatureValidator.sol new file mode 100644 index 000000000..1fd0eccf0 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/ISignatureValidator.sol @@ -0,0 +1,57 @@ +/* + + 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; + + +contract ISignatureValidator { + + /// @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; + + /// @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; + + /// @dev Verifies that a signature is valid. + /// @param hash Message hash that is signed. + /// @param signerAddress Address of signer. + /// @param signature Proof of signing. + /// @return Validity of order signature. + function isValidSignature( + bytes32 hash, + address signerAddress, + bytes memory signature + ) + public + view + returns (bool isValid); +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/ITransactions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/ITransactions.sol new file mode 100644 index 000000000..4446c55ce --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/ITransactions.sol @@ -0,0 +1,35 @@ +/* + + 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; + + +contract ITransactions { + + /// @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; +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IValidator.sol b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IValidator.sol new file mode 100644 index 000000000..2dd69100c --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IValidator.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; + + +contract IValidator { + + /// @dev Verifies that a signature is valid. + /// @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 + view + returns (bool isValid); +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IWallet.sol b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IWallet.sol new file mode 100644 index 000000000..c97161ca6 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IWallet.sol @@ -0,0 +1,35 @@ +/* + + 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; + + +contract IWallet { + + /// @dev Verifies that a signature is valid. + /// @param hash Message hash that is signed. + /// @param signature Proof of signing. + /// @return Validity of order signature. + function isValidSignature( + bytes32 hash, + bytes signature + ) + external + view + returns (bool isValid); +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IWrapperFunctions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IWrapperFunctions.sol new file mode 100644 index 000000000..56a533646 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/interfaces/IWrapperFunctions.sol @@ -0,0 +1,160 @@ +/* + + 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 "../libs/LibOrder.sol"; +import "../libs/LibFillResults.sol"; + + +contract 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 fillOrKillOrder( + LibOrder.Order memory order, + uint256 takerAssetFillAmount, + bytes memory signature + ) + public + returns (LibFillResults.FillResults memory fillResults); + + /// @dev Fills an order with specified parameters and ECDSA signature. + /// Returns false if the transaction would otherwise revert. + /// @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. + /// @return Amounts filled and fees paid by maker and taker. + function fillOrderNoThrow( + LibOrder.Order memory order, + uint256 takerAssetFillAmount, + bytes memory signature + ) + public + returns (LibFillResults.FillResults memory 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. + function batchFillOrders( + LibOrder.Order[] memory orders, + uint256[] memory takerAssetFillAmounts, + bytes[] memory signatures + ) + public + returns (LibFillResults.FillResults memory 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. + function batchFillOrKillOrders( + LibOrder.Order[] memory orders, + uint256[] memory takerAssetFillAmounts, + bytes[] memory signatures + ) + public + returns (LibFillResults.FillResults memory 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. + function batchFillOrdersNoThrow( + LibOrder.Order[] memory orders, + uint256[] memory takerAssetFillAmounts, + bytes[] memory signatures + ) + public + returns (LibFillResults.FillResults memory 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 + returns (LibFillResults.FillResults memory 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 (LibFillResults.FillResults memory 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 + returns (LibFillResults.FillResults memory 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 (LibFillResults.FillResults memory totalFillResults); + + /// @dev Synchronously cancels multiple orders in a single transaction. + /// @param orders Array of order specifications. + function batchCancelOrders(LibOrder.Order[] memory orders) + public; + + /// @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); +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibAbiEncoder.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibAbiEncoder.sol new file mode 100644 index 000000000..4aad37709 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibAbiEncoder.sol @@ -0,0 +1,215 @@ +/* + + 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 "./LibOrder.sol"; + + +contract LibAbiEncoder { + + /// @dev ABI encodes calldata for `fillOrder`. + /// @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 ABI encoded calldata for `fillOrder`. + function abiEncodeFillOrder( + LibOrder.Order memory order, + uint256 takerAssetFillAmount, + bytes memory signature + ) + internal + pure + returns (bytes memory fillOrderCalldata) + { + // We need to call MExchangeCore.fillOrder using a delegatecall in + // assembly so that we can intercept a call that throws. For this, we + // need the input encoded in memory in the Ethereum ABIv2 format [1]. + + // | Area | Offset | Length | Contents | + // | -------- |--------|---------|-------------------------------------------- | + // | Header | 0x00 | 4 | function selector | + // | Params | | 3 * 32 | function parameters: | + // | | 0x00 | | 1. offset to order (*) | + // | | 0x20 | | 2. takerAssetFillAmount | + // | | 0x40 | | 3. offset to signature (*) | + // | Data | | 12 * 32 | order: | + // | | 0x000 | | 1. senderAddress | + // | | 0x020 | | 2. makerAddress | + // | | 0x040 | | 3. takerAddress | + // | | 0x060 | | 4. feeRecipientAddress | + // | | 0x080 | | 5. makerAssetAmount | + // | | 0x0A0 | | 6. takerAssetAmount | + // | | 0x0C0 | | 7. makerFeeAmount | + // | | 0x0E0 | | 8. takerFeeAmount | + // | | 0x100 | | 9. expirationTimeSeconds | + // | | 0x120 | | 10. salt | + // | | 0x140 | | 11. Offset to makerAssetData (*) | + // | | 0x160 | | 12. Offset to takerAssetData (*) | + // | | 0x180 | 32 | makerAssetData Length | + // | | 0x1A0 | ** | makerAssetData Contents | + // | | 0x1C0 | 32 | takerAssetData Length | + // | | 0x1E0 | ** | takerAssetData Contents | + // | | 0x200 | 32 | signature Length | + // | | 0x220 | ** | signature Contents | + + // * Offsets are calculated from the beginning of the current area: Header, Params, Data: + // An offset stored in the Params area is calculated from the beginning of the Params section. + // An offset stored in the Data area is calculated from the beginning of the Data section. + + // ** The length of dynamic array contents are stored in the field immediately preceeding the contents. + + // [1]: https://solidity.readthedocs.io/en/develop/abi-spec.html + + assembly { + + // Areas below may use the following variables: + // 1. <area>Start -- Start of this area in memory + // 2. <area>End -- End of this area in memory. This value may + // be precomputed (before writing contents), + // or it may be computed as contents are written. + // 3. <area>Offset -- Current offset into area. If an area's End + // is precomputed, this variable tracks the + // offsets of contents as they are written. + + /////// Setup Header Area /////// + // Load free memory pointer + fillOrderCalldata := mload(0x40) + // bytes4(keccak256("fillOrder((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),uint256,bytes)")) + // = 0xb4be83d5 + // Leave 0x20 bytes to store the length + mstore(add(fillOrderCalldata, 0x20), 0xb4be83d500000000000000000000000000000000000000000000000000000000) + let headerAreaEnd := add(fillOrderCalldata, 0x24) + + /////// Setup Params Area /////// + // This area is preallocated and written to later. + // This is because we need to fill in offsets that have not yet been calculated. + let paramsAreaStart := headerAreaEnd + let paramsAreaEnd := add(paramsAreaStart, 0x60) + let paramsAreaOffset := paramsAreaStart + + /////// Setup Data Area /////// + let dataAreaStart := paramsAreaEnd + let dataAreaEnd := dataAreaStart + + // Offset from the source data we're reading from + let sourceOffset := order + // arrayLenBytes and arrayLenWords track the length of a dynamically-allocated bytes array. + let arrayLenBytes := 0 + let arrayLenWords := 0 + + /////// Write order Struct /////// + // Write memory location of Order, relative to the start of the + // parameter list, then increment the paramsAreaOffset respectively. + mstore(paramsAreaOffset, sub(dataAreaEnd, paramsAreaStart)) + paramsAreaOffset := add(paramsAreaOffset, 0x20) + + // Write values for each field in the order + // It would be nice to use a loop, but we save on gas by writing + // the stores sequentially. + mstore(dataAreaEnd, mload(sourceOffset)) // makerAddress + mstore(add(dataAreaEnd, 0x20), mload(add(sourceOffset, 0x20))) // takerAddress + mstore(add(dataAreaEnd, 0x40), mload(add(sourceOffset, 0x40))) // feeRecipientAddress + mstore(add(dataAreaEnd, 0x60), mload(add(sourceOffset, 0x60))) // senderAddress + mstore(add(dataAreaEnd, 0x80), mload(add(sourceOffset, 0x80))) // makerAssetAmount + mstore(add(dataAreaEnd, 0xA0), mload(add(sourceOffset, 0xA0))) // takerAssetAmount + mstore(add(dataAreaEnd, 0xC0), mload(add(sourceOffset, 0xC0))) // makerFeeAmount + mstore(add(dataAreaEnd, 0xE0), mload(add(sourceOffset, 0xE0))) // takerFeeAmount + mstore(add(dataAreaEnd, 0x100), mload(add(sourceOffset, 0x100))) // expirationTimeSeconds + mstore(add(dataAreaEnd, 0x120), mload(add(sourceOffset, 0x120))) // salt + mstore(add(dataAreaEnd, 0x140), mload(add(sourceOffset, 0x140))) // Offset to makerAssetData + mstore(add(dataAreaEnd, 0x160), mload(add(sourceOffset, 0x160))) // Offset to takerAssetData + dataAreaEnd := add(dataAreaEnd, 0x180) + sourceOffset := add(sourceOffset, 0x180) + + // Write offset to <order.makerAssetData> + mstore(add(dataAreaStart, mul(10, 0x20)), sub(dataAreaEnd, dataAreaStart)) + + // Calculate length of <order.makerAssetData> + sourceOffset := mload(add(order, 0x140)) // makerAssetData + arrayLenBytes := mload(sourceOffset) + sourceOffset := add(sourceOffset, 0x20) + arrayLenWords := div(add(arrayLenBytes, 0x1F), 0x20) + + // Write length of <order.makerAssetData> + mstore(dataAreaEnd, arrayLenBytes) + dataAreaEnd := add(dataAreaEnd, 0x20) + + // Write contents of <order.makerAssetData> + for {let i := 0} lt(i, arrayLenWords) {i := add(i, 1)} { + mstore(dataAreaEnd, mload(sourceOffset)) + dataAreaEnd := add(dataAreaEnd, 0x20) + sourceOffset := add(sourceOffset, 0x20) + } + + // Write offset to <order.takerAssetData> + mstore(add(dataAreaStart, mul(11, 0x20)), sub(dataAreaEnd, dataAreaStart)) + + // Calculate length of <order.takerAssetData> + sourceOffset := mload(add(order, 0x160)) // takerAssetData + arrayLenBytes := mload(sourceOffset) + sourceOffset := add(sourceOffset, 0x20) + arrayLenWords := div(add(arrayLenBytes, 0x1F), 0x20) + + // Write length of <order.takerAssetData> + mstore(dataAreaEnd, arrayLenBytes) + dataAreaEnd := add(dataAreaEnd, 0x20) + + // Write contents of <order.takerAssetData> + for {let i := 0} lt(i, arrayLenWords) {i := add(i, 1)} { + mstore(dataAreaEnd, mload(sourceOffset)) + dataAreaEnd := add(dataAreaEnd, 0x20) + sourceOffset := add(sourceOffset, 0x20) + } + + /////// Write takerAssetFillAmount /////// + mstore(paramsAreaOffset, takerAssetFillAmount) + paramsAreaOffset := add(paramsAreaOffset, 0x20) + + /////// Write signature /////// + // Write offset to paramsArea + mstore(paramsAreaOffset, sub(dataAreaEnd, paramsAreaStart)) + + // Calculate length of signature + sourceOffset := signature + arrayLenBytes := mload(sourceOffset) + sourceOffset := add(sourceOffset, 0x20) + arrayLenWords := div(add(arrayLenBytes, 0x1F), 0x20) + + // Write length of signature + mstore(dataAreaEnd, arrayLenBytes) + dataAreaEnd := add(dataAreaEnd, 0x20) + + // Write contents of signature + for {let i := 0} lt(i, arrayLenWords) {i := add(i, 1)} { + mstore(dataAreaEnd, mload(sourceOffset)) + dataAreaEnd := add(dataAreaEnd, 0x20) + sourceOffset := add(sourceOffset, 0x20) + } + + // Set length of calldata + mstore(fillOrderCalldata, sub(dataAreaEnd, add(fillOrderCalldata, 0x20))) + + // Increment free memory pointer + mstore(0x40, dataAreaEnd) + } + + return fillOrderCalldata; + } +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibConstants.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibConstants.sol new file mode 100644 index 000000000..8d2732cd3 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibConstants.sol @@ -0,0 +1,49 @@ +/* + + 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; + + +// solhint-disable max-line-length +contract LibConstants { + + // Asset data for ZRX token. Used for fee transfers. + // @TODO: Hardcode constant when we deploy. Currently + // not constant to make testing easier. + + // The proxyId for ZRX_ASSET_DATA is bytes4(keccak256("ERC20Token(address)")) = 0xf47261b0 + + // Kovan ZRX address is 0x6ff6c0ff1d68b964901f986d4c9fa3ac68346570. + // The ABI encoded proxyId and address is 0xf47261b00000000000000000000000006ff6c0ff1d68b964901f986d4c9fa3ac68346570 + // bytes constant public ZRX_ASSET_DATA = "\xf4\x72\x61\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x6f\xf6\xc0\xff\x1d\x68\xb9\x64\x90\x1f\x98\x6d\x4c\x9f\xa3\xac\x68\x34\x65\x70"; + + // Mainnet ZRX address is 0xe41d2489571d322189246dafa5ebde1f4699f498. + // The ABI encoded proxyId and address is 0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498 + // bytes constant public ZRX_ASSET_DATA = "\xf4\x72\x61\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe4\x1d\x24\x89\x57\x1d\x32\x21\x89\x24\x6d\xaf\xa5\xeb\xde\x1f\x46\x99\xf4\x98"; + + // solhint-disable-next-line var-name-mixedcase + bytes public ZRX_ASSET_DATA; + + // @TODO: Remove when we deploy. + constructor (bytes memory zrxAssetData) + public + { + ZRX_ASSET_DATA = zrxAssetData; + } +} +// solhint-enable max-line-length diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibEIP712.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibEIP712.sol new file mode 100644 index 000000000..b02f7632e --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibEIP712.sol @@ -0,0 +1,86 @@ +/* + + 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; + + +contract LibEIP712 { + // EIP191 header for EIP712 prefix + string constant internal EIP191_HEADER = "\x19\x01"; + + // EIP712 Domain Name value + string constant internal EIP712_DOMAIN_NAME = "0x Protocol"; + + // EIP712 Domain Version value + string constant internal EIP712_DOMAIN_VERSION = "2"; + + // Hash of the EIP712 Domain Separator Schema + bytes32 constant internal EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH = keccak256(abi.encodePacked( + "EIP712Domain(", + "string name,", + "string version,", + "address verifyingContract", + ")" + )); + + // Hash of the EIP712 Domain Separator data + // solhint-disable-next-line var-name-mixedcase + bytes32 public EIP712_DOMAIN_HASH; + + constructor () + public + { + EIP712_DOMAIN_HASH = keccak256(abi.encodePacked( + EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH, + keccak256(bytes(EIP712_DOMAIN_NAME)), + keccak256(bytes(EIP712_DOMAIN_VERSION)), + bytes32(address(this)) + )); + } + + /// @dev Calculates EIP712 encoding for a hash struct in this EIP712 Domain. + /// @param hashStruct The EIP712 hash struct. + /// @return EIP712 hash applied to this EIP712 Domain. + function hashEIP712Message(bytes32 hashStruct) + internal + view + returns (bytes32 result) + { + bytes32 eip712DomainHash = EIP712_DOMAIN_HASH; + + // Assembly for more efficient computing: + // keccak256(abi.encodePacked( + // EIP191_HEADER, + // EIP712_DOMAIN_HASH, + // hashStruct + // )); + + assembly { + // Load free memory pointer + let memPtr := mload(64) + + mstore(memPtr, 0x1901000000000000000000000000000000000000000000000000000000000000) // EIP191 header + mstore(add(memPtr, 2), eip712DomainHash) // EIP712 domain hash + mstore(add(memPtr, 34), hashStruct) // Hash of struct + + // Compute hash + result := keccak256(memPtr, 66) + } + return result; + } +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibExchangeErrors.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibExchangeErrors.sol new file mode 100644 index 000000000..a0f75bc06 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibExchangeErrors.sol @@ -0,0 +1,70 @@ +/* + + 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. + +*/ + +// solhint-disable +pragma solidity 0.4.24; + + +/// @dev This contract documents the revert reasons used in the Exchange contract. +/// This contract is intended to serve as a reference, but is not actually used for efficiency reasons. +contract LibExchangeErrors { + + /// Order validation errors /// + string constant ORDER_UNFILLABLE = "ORDER_UNFILLABLE"; // Order cannot be filled. + string constant INVALID_MAKER = "INVALID_MAKER"; // Invalid makerAddress. + string constant INVALID_TAKER = "INVALID_TAKER"; // Invalid takerAddress. + string constant INVALID_SENDER = "INVALID_SENDER"; // Invalid `msg.sender`. + string constant INVALID_ORDER_SIGNATURE = "INVALID_ORDER_SIGNATURE"; // Signature validation failed. + + /// fillOrder validation errors /// + string constant INVALID_TAKER_AMOUNT = "INVALID_TAKER_AMOUNT"; // takerAssetFillAmount cannot equal 0. + string constant ROUNDING_ERROR = "ROUNDING_ERROR"; // Rounding error greater than 0.1% of takerAssetFillAmount. + + /// Signature validation errors /// + string constant INVALID_SIGNATURE = "INVALID_SIGNATURE"; // Signature validation failed. + string constant SIGNATURE_ILLEGAL = "SIGNATURE_ILLEGAL"; // Signature type is illegal. + string constant SIGNATURE_UNSUPPORTED = "SIGNATURE_UNSUPPORTED"; // Signature type unsupported. + + /// cancelOrdersUptTo errors /// + string constant INVALID_NEW_ORDER_EPOCH = "INVALID_NEW_ORDER_EPOCH"; // Specified salt must be greater than or equal to existing orderEpoch. + + /// fillOrKillOrder errors /// + string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired takerAssetFillAmount could not be completely filled. + + /// matchOrders errors /// + string constant NEGATIVE_SPREAD_REQUIRED = "NEGATIVE_SPREAD_REQUIRED"; // Matched orders must have a negative spread. + + /// Transaction errors /// + string constant REENTRANCY_ILLEGAL = "REENTRANCY_ILLEGAL"; // Recursive reentrancy is not allowed. + string constant INVALID_TX_HASH = "INVALID_TX_HASH"; // Transaction has already been executed. + string constant INVALID_TX_SIGNATURE = "INVALID_TX_SIGNATURE"; // Signature validation failed. + string constant FAILED_EXECUTION = "FAILED_EXECUTION"; // Transaction execution failed. + + /// registerAssetProxy errors /// + string constant ASSET_PROXY_ALREADY_EXISTS = "ASSET_PROXY_ALREADY_EXISTS"; // AssetProxy with same id already exists. + + /// dispatchTransferFrom errors /// + string constant ASSET_PROXY_DOES_NOT_EXIST = "ASSET_PROXY_DOES_NOT_EXIST"; // No assetProxy registered at given id. + string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Asset transfer unsuccesful. + + /// Length validation errors /// + string constant LENGTH_GREATER_THAN_0_REQUIRED = "LENGTH_GREATER_THAN_0_REQUIRED"; // Byte array must have a length greater than 0. + string constant LENGTH_GREATER_THAN_3_REQUIRED = "LENGTH_GREATER_THAN_3_REQUIRED"; // Byte array must have a length greater than 3. + string constant LENGTH_0_REQUIRED = "LENGTH_0_REQUIRED"; // Byte array must have a length of 0. + string constant LENGTH_65_REQUIRED = "LENGTH_65_REQUIRED"; // Byte array must have a length of 65. +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibFillResults.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibFillResults.sol new file mode 100644 index 000000000..1b4181d94 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibFillResults.sol @@ -0,0 +1,54 @@ +/* + + 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 "../../../utils/SafeMath/SafeMath.sol"; + + +contract LibFillResults is + SafeMath +{ + + struct FillResults { + uint256 makerAssetFilledAmount; // Total amount of makerAsset(s) filled. + uint256 takerAssetFilledAmount; // Total amount of takerAsset(s) filled. + uint256 makerFeePaid; // Total amount of ZRX paid by maker(s) to feeRecipient(s). + uint256 takerFeePaid; // Total amount of ZRX paid by taker to feeRecipients(s). + } + + struct MatchedFillResults { + FillResults left; // Amounts filled and fees paid of left order. + FillResults right; // Amounts filled and fees paid of right order. + uint256 leftMakerAssetSpreadAmount; // Spread between price of left and right order, denominated in the left order's makerAsset, paid to taker. + } + + /// @dev Adds properties of both FillResults instances. + /// Modifies the first FillResults instance specified. + /// @param totalFillResults Fill results instance that will be added onto. + /// @param singleFillResults Fill results instance that will be added to totalFillResults. + function addFillResults(FillResults memory totalFillResults, FillResults memory singleFillResults) + internal + pure + { + totalFillResults.makerAssetFilledAmount = safeAdd(totalFillResults.makerAssetFilledAmount, singleFillResults.makerAssetFilledAmount); + totalFillResults.takerAssetFilledAmount = safeAdd(totalFillResults.takerAssetFilledAmount, singleFillResults.takerAssetFilledAmount); + totalFillResults.makerFeePaid = safeAdd(totalFillResults.makerFeePaid, singleFillResults.makerFeePaid); + totalFillResults.takerFeePaid = safeAdd(totalFillResults.takerFeePaid, singleFillResults.takerFeePaid); + } +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol new file mode 100644 index 000000000..fa09da6ac --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.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 "../../../utils/SafeMath/SafeMath.sol"; + + +contract LibMath is + SafeMath +{ + + /// @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 getPartialAmount( + uint256 numerator, + uint256 denominator, + uint256 target + ) + internal + pure + returns (uint256 partialAmount) + { + partialAmount = safeDiv( + safeMul(numerator, target), + denominator + ); + return partialAmount; + } + + /// @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 isRoundingError( + uint256 numerator, + uint256 denominator, + uint256 target + ) + internal + pure + returns (bool isError) + { + uint256 remainder = mulmod(target, numerator, denominator); + if (remainder == 0) { + return false; // No rounding error. + } + + uint256 errPercentageTimes1000000 = safeDiv( + safeMul(remainder, 1000000), + safeMul(numerator, target) + ); + isError = errPercentageTimes1000000 > 1000; + return isError; + } +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol new file mode 100644 index 000000000..68f4f5f1b --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibOrder.sol @@ -0,0 +1,146 @@ +/* + + 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 "./LibEIP712.sol"; + + +contract LibOrder is + LibEIP712 +{ + + // Hash for the EIP712 Order Schema + bytes32 constant internal EIP712_ORDER_SCHEMA_HASH = keccak256(abi.encodePacked( + "Order(", + "address makerAddress,", + "address takerAddress,", + "address feeRecipientAddress,", + "address senderAddress,", + "uint256 makerAssetAmount,", + "uint256 takerAssetAmount,", + "uint256 makerFee,", + "uint256 takerFee,", + "uint256 expirationTimeSeconds,", + "uint256 salt,", + "bytes makerAssetData,", + "bytes takerAssetData", + ")" + )); + + // A valid order remains fillable until it is expired, fully filled, or cancelled. + // An order's state is unaffected by external factors, like account balances. + enum OrderStatus { + INVALID, // Default value + INVALID_MAKER_ASSET_AMOUNT, // Order does not have a valid maker asset amount + INVALID_TAKER_ASSET_AMOUNT, // Order does not have a valid taker asset amount + FILLABLE, // Order is fillable + EXPIRED, // Order has already expired + FULLY_FILLED, // Order is fully filled + CANCELLED // Order has been cancelled + } + + // solhint-disable max-line-length + struct Order { + address makerAddress; // Address that created the order. + address takerAddress; // Address that is allowed to fill the order. If set to 0, any address is allowed to fill the order. + address feeRecipientAddress; // Address that will recieve fees when order is filled. + address senderAddress; // Address that is allowed to call Exchange contract methods that affect this order. If set to 0, any address is allowed to call these methods. + uint256 makerAssetAmount; // Amount of makerAsset being offered by maker. Must be greater than 0. + uint256 takerAssetAmount; // Amount of takerAsset being bid on by maker. Must be greater than 0. + uint256 makerFee; // Amount of ZRX paid to feeRecipient by maker when order is filled. If set to 0, no transfer of ZRX from maker to feeRecipient will be attempted. + uint256 takerFee; // Amount of ZRX paid to feeRecipient by taker when order is filled. If set to 0, no transfer of ZRX from taker to feeRecipient will be attempted. + uint256 expirationTimeSeconds; // Timestamp in seconds at which order expires. + uint256 salt; // Arbitrary number to facilitate uniqueness of the order's hash. + bytes makerAssetData; // Encoded data that can be decoded by a specified proxy contract when transferring makerAsset. The last byte references the id of this proxy. + bytes takerAssetData; // Encoded data that can be decoded by a specified proxy contract when transferring takerAsset. The last byte references the id of this proxy. + } + // solhint-enable max-line-length + + struct OrderInfo { + uint8 orderStatus; // Status that describes order's validity and fillability. + bytes32 orderHash; // EIP712 hash of the order (see LibOrder.getOrderHash). + uint256 orderTakerAssetFilledAmount; // Amount of order that has already been filled. + } + + /// @dev Calculates Keccak-256 hash of the order. + /// @param order The order structure. + /// @return Keccak-256 EIP712 hash of the order. + function getOrderHash(Order memory order) + internal + view + returns (bytes32 orderHash) + { + orderHash = hashEIP712Message(hashOrder(order)); + return orderHash; + } + + /// @dev Calculates EIP712 hash of the order. + /// @param order The order structure. + /// @return EIP712 hash of the order. + function hashOrder(Order memory order) + internal + pure + returns (bytes32 result) + { + bytes32 schemaHash = EIP712_ORDER_SCHEMA_HASH; + bytes32 makerAssetDataHash = keccak256(order.makerAssetData); + bytes32 takerAssetDataHash = keccak256(order.takerAssetData); + + // Assembly for more efficiently computing: + // keccak256(abi.encodePacked( + // EIP712_ORDER_SCHEMA_HASH, + // bytes32(order.makerAddress), + // bytes32(order.takerAddress), + // bytes32(order.feeRecipientAddress), + // bytes32(order.senderAddress), + // order.makerAssetAmount, + // order.takerAssetAmount, + // order.makerFee, + // order.takerFee, + // order.expirationTimeSeconds, + // order.salt, + // keccak256(order.makerAssetData), + // keccak256(order.takerAssetData) + // )); + + assembly { + // Calculate memory addresses that will be swapped out before hashing + let pos1 := sub(order, 32) + let pos2 := add(order, 320) + let pos3 := add(order, 352) + + // Backup + let temp1 := mload(pos1) + let temp2 := mload(pos2) + let temp3 := mload(pos3) + + // Hash in place + mstore(pos1, schemaHash) + mstore(pos2, makerAssetDataHash) + mstore(pos3, takerAssetDataHash) + result := keccak256(pos1, 416) + + // Restore + mstore(pos1, temp1) + mstore(pos2, temp2) + mstore(pos3, temp3) + } + return result; + } +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MAssetProxyDispatcher.sol b/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MAssetProxyDispatcher.sol new file mode 100644 index 000000000..c6904300a --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MAssetProxyDispatcher.sol @@ -0,0 +1,46 @@ +/* + + 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 "../interfaces/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/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MExchangeCore.sol b/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MExchangeCore.sol new file mode 100644 index 000000000..c165b647c --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MExchangeCore.sol @@ -0,0 +1,127 @@ +/* + + 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 "../libs/LibOrder.sol"; +import "../libs/LibFillResults.sol"; +import "../interfaces/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 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 Status, orderHash, and amount already filled of order. + /// @param takerAddress Address of order taker. + /// @param takerAssetFillAmount Desired amount of order to fill by taker. + /// @param takerAssetFilledAmount Amount of takerAsset that will be filled. + /// @param signature Proof that the orders was created by its maker. + function assertValidFill( + LibOrder.Order memory order, + LibOrder.OrderInfo memory orderInfo, + address takerAddress, + uint256 takerAssetFillAmount, + uint256 takerAssetFilledAmount, + bytes memory signature + ) + 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/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MMatchOrders.sol b/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MMatchOrders.sol new file mode 100644 index 000000000..a31ec1585 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MMatchOrders.sol @@ -0,0 +1,59 @@ +/* + + 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 "../libs/LibOrder.sol"; +import "../libs/LibFillResults.sol"; +import "../interfaces/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/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MSignatureValidator.sol b/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MSignatureValidator.sol new file mode 100644 index 000000000..f14f2ba00 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MSignatureValidator.sol @@ -0,0 +1,46 @@ +/* + + 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 "../interfaces/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 + Caller, // 0x04 + Wallet, // 0x05 + Validator, // 0x06 + PreSigned, // 0x07 + Trezor, // 0x08 + NSignatureTypes // 0x09, number of signature types. Always leave at end. + } +} diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MTransactions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MTransactions.sol new file mode 100644 index 000000000..f2b5e4b16 --- /dev/null +++ b/packages/contracts/src/2.0.0/protocol/Exchange/mixins/MTransactions.sol @@ -0,0 +1,36 @@ +/* + + 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 "../interfaces/ITransactions.sol"; + + +contract MTransactions is + ITransactions +{ + + /// @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/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol new file mode 100644 index 000000000..9272b18a8 --- /dev/null +++ b/packages/contracts/src/2.0.0/test/DummyERC20Token/DummyERC20Token.sol @@ -0,0 +1,60 @@ +/* + + 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 "../Mintable/Mintable.sol"; +import "../../utils/Ownable/Ownable.sol"; + + +contract DummyERC20Token is + Mintable, + Ownable +{ + string public name; + string public symbol; + uint256 public decimals; + + constructor ( + string _name, + string _symbol, + uint256 _decimals, + uint256 _totalSupply + ) + public + { + name = _name; + symbol = _symbol; + decimals = _decimals; + totalSupply = _totalSupply; + balances[msg.sender] = _totalSupply; + } + + function setBalance(address _target, uint256 _value) + public + onlyOwner + { + uint256 currBalance = balanceOf(_target); + if (_value < currBalance) { + totalSupply = safeSub(totalSupply, safeSub(currBalance, _value)); + } else { + totalSupply = safeAdd(totalSupply, safeSub(_value, currBalance)); + } + balances[_target] = _value; + } +} diff --git a/packages/contracts/src/2.0.0/test/DummyERC721Receiver/DummyERC721Receiver.sol b/packages/contracts/src/2.0.0/test/DummyERC721Receiver/DummyERC721Receiver.sol new file mode 100644 index 000000000..5dce74a14 --- /dev/null +++ b/packages/contracts/src/2.0.0/test/DummyERC721Receiver/DummyERC721Receiver.sol @@ -0,0 +1,64 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016 Smart Contract Solutions, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +pragma solidity 0.4.24; + +import "../../tokens/ERC721Token/IERC721Receiver.sol"; + + +contract DummyERC721Receiver is + IERC721Receiver +{ + + event TokenReceived( + address from, + uint256 tokenId, + bytes data + ); + + /** + * @notice Handle the receipt of an NFT + * @dev The ERC721 smart contract calls this function on the recipient + * after a `safetransfer`. This function MAY throw to revert and reject the + * transfer. This function MUST use 50,000 gas or less. Return of other + * than the magic value MUST result in the transaction being reverted. + * Note: the contract address is always the message sender. + * @param _from The sending address + * @param _tokenId The NFT identifier which is being transfered + * @param _data Additional data with no specified format + * @return `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))` + */ + function onERC721Received( + address _from, + uint256 _tokenId, + bytes _data + ) + public + returns (bytes4) + { + emit TokenReceived(_from, _tokenId, _data); + return ERC721_RECEIVED; + } +} diff --git a/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol b/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol new file mode 100644 index 000000000..627746a52 --- /dev/null +++ b/packages/contracts/src/2.0.0/test/DummyERC721Token/DummyERC721Token.sol @@ -0,0 +1,76 @@ +/* + + 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 "../../tokens/ERC721Token/ERC721Token.sol"; +import "../../utils/Ownable/Ownable.sol"; + + +// solhint-disable no-empty-blocks +contract DummyERC721Token is + Ownable, + ERC721Token +{ + + /** + * @dev Constructor passes its arguments to the base ERC721Token constructor + * @param name of token + * @param symbol of token + */ + constructor ( + string name, + string symbol + ) + public + ERC721Token(name, symbol) + {} + + /** + * @dev Function to mint a new token + * @dev Reverts if the given token ID already exists + * @param to address the beneficiary that will own the minted token + * @param tokenId uint256 ID of the token to be minted by the msg.sender + */ + function mint(address to, uint256 tokenId) + public + onlyOwner + { + require( + !exists(tokenId), + "Token with tokenId already exists." + ); + _mint(to, tokenId); + } + + /** + * @dev Function to burn a token + * @dev Reverts if the given token ID doesn't exist + * @param tokenId uint256 ID of the token to be minted by the msg.sender + */ + function burn(address owner, uint256 tokenId) + public + onlyOwner + { + require( + exists(tokenId), + "Token with tokenId does not exist." + ); + _burn(owner, tokenId); + } +} diff --git a/packages/contracts/src/2.0.0/test/Mintable/Mintable.sol b/packages/contracts/src/2.0.0/test/Mintable/Mintable.sol new file mode 100644 index 000000000..767cc8d25 --- /dev/null +++ b/packages/contracts/src/2.0.0/test/Mintable/Mintable.sol @@ -0,0 +1,43 @@ +/* + + 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 "../../tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol"; +import "../../utils/SafeMath/SafeMath.sol"; + + +/* + * Mintable + * Base contract that creates a mintable UnlimitedAllowanceToken + */ +contract Mintable is + UnlimitedAllowanceToken, + SafeMath +{ + function mint(uint256 _value) + public + { + require( + _value <= 100000000000000000000, + "Minting more than 100000000000000000000 is not allowed." + ); + balances[msg.sender] = safeAdd(_value, balances[msg.sender]); + totalSupply = safeAdd(totalSupply, _value); + } +} diff --git a/packages/contracts/src/2.0.0/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol b/packages/contracts/src/2.0.0/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol new file mode 100644 index 000000000..ad71fc9a1 --- /dev/null +++ b/packages/contracts/src/2.0.0/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/packages/contracts/src/2.0.0/test/TestAssetProxyOwner/TestAssetProxyOwner.sol b/packages/contracts/src/2.0.0/test/TestAssetProxyOwner/TestAssetProxyOwner.sol new file mode 100644 index 000000000..75e782d43 --- /dev/null +++ b/packages/contracts/src/2.0.0/test/TestAssetProxyOwner/TestAssetProxyOwner.sol @@ -0,0 +1,72 @@ +/* + + 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.10; + +import "../../protocol/AssetProxyOwner/AssetProxyOwner.sol"; + + +// solhint-disable no-empty-blocks +contract TestAssetProxyOwner is + AssetProxyOwner +{ + + function TestAssetProxyOwner( + address[] memory _owners, + address[] memory _assetProxyContracts, + uint256 _required, + uint256 _secondsTimeLocked + ) + public + AssetProxyOwner(_owners, _assetProxyContracts, _required, _secondsTimeLocked) + {} + + function testValidRemoveAuthorizedAddressAtIndexTx(uint256 id) + public + 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 + returns (bool) + { + return readBytes4(data, 0) == REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR; + } + + /// @dev Reads an unpadded bytes4 value from a position in a byte array. + /// @param b Byte array containing a bytes4 value. + /// @param index Index in byte array of bytes4 value. + /// @return bytes4 value from byte array. + function publicReadBytes4( + bytes memory b, + uint256 index + ) + public + returns (bytes4 result) + { + result = readBytes4(b, index); + return result; + } +} diff --git a/packages/contracts/src/2.0.0/test/TestConstants/TestConstants.sol b/packages/contracts/src/2.0.0/test/TestConstants/TestConstants.sol new file mode 100644 index 000000000..1275d007b --- /dev/null +++ b/packages/contracts/src/2.0.0/test/TestConstants/TestConstants.sol @@ -0,0 +1,57 @@ +/* + + 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 "../../utils/LibBytes/LibBytes.sol"; + + +// solhint-disable max-line-length +contract TestConstants { + + using LibBytes for bytes; + + bytes4 constant internal ERC20_PROXY_ID = bytes4(keccak256("ERC20Token(address)")); + + address constant internal KOVAN_ZRX_ADDRESS = 0x6Ff6C0Ff1d68b964901F986d4C9FA3ac68346570; + bytes constant internal KOVAN_ZRX_ASSET_DATA = "\xf4\x72\x61\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x6f\xf6\xc0\xff\x1d\x68\xb9\x64\x90\x1f\x98\x6d\x4c\x9f\xa3\xac\x68\x34\x65\x70"; + + address constant internal MAINNET_ZRX_ADDRESS = 0xE41d2489571d322189246DaFA5ebDe1F4699F498; + bytes constant public MAINNET_ZRX_ASSET_DATA = "\xf4\x72\x61\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe4\x1d\x24\x89\x57\x1d\x32\x21\x89\x24\x6d\xaf\xa5\xeb\xde\x1f\x46\x99\xf4\x98"; + + function assertValidZrxAssetData() + public + pure + returns (bool) + { + bytes memory kovanZrxAssetData = abi.encodeWithSelector(ERC20_PROXY_ID, KOVAN_ZRX_ADDRESS); + require( + kovanZrxAssetData.equals(KOVAN_ZRX_ASSET_DATA), + "INVALID_KOVAN_ZRX_ASSET_DATA" + ); + + bytes memory mainetZrxAssetData = abi.encodeWithSelector(ERC20_PROXY_ID, MAINNET_ZRX_ADDRESS); + require( + mainetZrxAssetData.equals(MAINNET_ZRX_ASSET_DATA), + "INVALID_MAINNET_ZRX_ASSET_DATA" + ); + + return true; + } +} +// solhint-enable max-line-length
\ No newline at end of file diff --git a/packages/contracts/src/2.0.0/test/TestExchangeInternals/TestExchangeInternals.sol b/packages/contracts/src/2.0.0/test/TestExchangeInternals/TestExchangeInternals.sol new file mode 100644 index 000000000..d9cec9edc --- /dev/null +++ b/packages/contracts/src/2.0.0/test/TestExchangeInternals/TestExchangeInternals.sol @@ -0,0 +1,121 @@ +/* + + 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. + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to calculate partial of. + /// @return Partial value of target. + function publicGetPartialAmount( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (uint256 partialAmount) + { + return getPartialAmount(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 publicIsRoundingError( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (bool isError) + { + return isRoundingError(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/packages/contracts/src/2.0.0/test/TestLibBytes/TestLibBytes.sol b/packages/contracts/src/2.0.0/test/TestLibBytes/TestLibBytes.sol new file mode 100644 index 000000000..00d861e61 --- /dev/null +++ b/packages/contracts/src/2.0.0/test/TestLibBytes/TestLibBytes.sol @@ -0,0 +1,269 @@ +/* + + 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 "../../utils/LibBytes/LibBytes.sol"; + + +contract TestLibBytes { + + using LibBytes for bytes; + + /// @dev Pops the last byte off of a byte array by modifying its length. + /// @param b Byte array that will be modified. + /// @return The byte that was popped off. + function publicPopLastByte(bytes memory b) + public + pure + returns (bytes memory, bytes1 result) + { + result = b.popLastByte(); + return (b, result); + } + + /// @dev Pops the last 20 bytes off of a byte array by modifying its length. + /// @param b Byte array that will be modified. + /// @return The 20 byte address that was popped off. + function publicPopLast20Bytes(bytes memory b) + public + pure + returns (bytes memory, address result) + { + result = b.popLast20Bytes(); + return (b, result); + } + + /// @dev Tests equality of two byte arrays. + /// @param lhs First byte array to compare. + /// @param rhs Second byte array to compare. + /// @return True if arrays are the same. False otherwise. + function publicEquals(bytes memory lhs, bytes memory rhs) + public + pure + returns (bool equal) + { + equal = lhs.equals(rhs); + return equal; + } + + function publicEqualsPop1(bytes memory lhs, bytes memory rhs) + public + pure + returns (bool equal) + { + lhs.popLastByte(); + rhs.popLastByte(); + equal = lhs.equals(rhs); + return equal; + } + + /// @dev Performs a deep copy of a byte array onto another byte array of greater than or equal length. + /// @param dest Byte array that will be overwritten with source bytes. + /// @param source Byte array to copy onto dest bytes. + function publicDeepCopyBytes( + bytes memory dest, + bytes memory source + ) + public + pure + returns (bytes memory) + { + LibBytes.deepCopyBytes(dest, source); + return dest; + } + + /// @dev Reads an address from a position in a byte array. + /// @param b Byte array containing an address. + /// @param index Index in byte array of address. + /// @return address from byte array. + function publicReadAddress( + bytes memory b, + uint256 index + ) + public + pure + returns (address result) + { + result = b.readAddress(index); + return result; + } + + /// @dev Writes an address into a specific position in a byte array. + /// @param b Byte array to insert address into. + /// @param index Index in byte array of address. + /// @param input Address to put into byte array. + function publicWriteAddress( + bytes memory b, + uint256 index, + address input + ) + public + pure + returns (bytes memory) + { + b.writeAddress(index, input); + return b; + } + + /// @dev Reads a bytes32 value from a position in a byte array. + /// @param b Byte array containing a bytes32 value. + /// @param index Index in byte array of bytes32 value. + /// @return bytes32 value from byte array. + function publicReadBytes32( + bytes memory b, + uint256 index + ) + public + pure + returns (bytes32 result) + { + result = b.readBytes32(index); + return result; + } + + /// @dev Writes a bytes32 into a specific position in a byte array. + /// @param b Byte array to insert <input> into. + /// @param index Index in byte array of <input>. + /// @param input bytes32 to put into byte array. + function publicWriteBytes32( + bytes memory b, + uint256 index, + bytes32 input + ) + public + pure + returns (bytes memory) + { + b.writeBytes32(index, input); + return b; + } + + /// @dev Reads a uint256 value from a position in a byte array. + /// @param b Byte array containing a uint256 value. + /// @param index Index in byte array of uint256 value. + /// @return uint256 value from byte array. + function publicReadUint256( + bytes memory b, + uint256 index + ) + public + pure + returns (uint256 result) + { + result = b.readUint256(index); + return result; + } + + /// @dev Writes a uint256 into a specific position in a byte array. + /// @param b Byte array to insert <input> into. + /// @param index Index in byte array of <input>. + /// @param input uint256 to put into byte array. + function publicWriteUint256( + bytes memory b, + uint256 index, + uint256 input + ) + public + pure + returns (bytes memory) + { + b.writeUint256(index, input); + return b; + } + + /// @dev Reads an unpadded bytes4 value from a position in a byte array. + /// @param b Byte array containing a bytes4 value. + /// @param index Index in byte array of bytes4 value. + /// @return bytes4 value from byte array. + function publicReadBytes4( + bytes memory b, + uint256 index + ) + public + pure + returns (bytes4 result) + { + result = b.readBytes4(index); + return result; + } + + /// @dev Reads nested bytes from a specific position. + /// @param b Byte array containing nested bytes. + /// @param index Index of nested bytes. + /// @return result Nested bytes. + function publicReadBytesWithLength( + bytes memory b, + uint256 index + ) + public + pure + returns (bytes memory result) + { + result = b.readBytesWithLength(index); + return result; + } + + /// @dev Inserts bytes at a specific position in a byte array. + /// @param b Byte array to insert <input> into. + /// @param index Index in byte array of <input>. + /// @param input bytes to insert. + /// @return b Updated input byte array + function publicWriteBytesWithLength( + bytes memory b, + uint256 index, + bytes memory input + ) + public + pure + returns (bytes memory) + { + b.writeBytesWithLength(index, input); + return b; + } + + /// @dev Copies a block of memory from one location to another. + /// @param mem Memory contents we want to apply memCopy to + /// @param dest Destination offset into <mem>. + /// @param source Source offset into <mem>. + /// @param length Length of bytes to copy from <source> to <dest> + /// @return mem Memory contents after calling memCopy. + function testMemcpy( + bytes mem, + uint256 dest, + uint256 source, + uint256 length + ) + public // not external, we need input in memory + pure + returns (bytes) + { + // Sanity check. Overflows are not checked. + require(source + length <= mem.length); + require(dest + length <= mem.length); + + // Get pointer to memory contents + uint256 offset = mem.contentAddress(); + + // Execute memCopy adjusted for memory array location + LibBytes.memCopy(offset + dest, offset + source, length); + + // Return modified memory contents + return mem; + } +} diff --git a/packages/contracts/src/2.0.0/test/TestLibs/TestLibs.sol b/packages/contracts/src/2.0.0/test/TestLibs/TestLibs.sol new file mode 100644 index 000000000..4a99dd9c1 --- /dev/null +++ b/packages/contracts/src/2.0.0/test/TestLibs/TestLibs.sol @@ -0,0 +1,119 @@ +/* + + 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/libs/LibMath.sol"; +import "../../protocol/Exchange/libs/LibOrder.sol"; +import "../../protocol/Exchange/libs/LibFillResults.sol"; +import "../../protocol/Exchange/libs/LibAbiEncoder.sol"; + + +contract TestLibs is + LibMath, + LibOrder, + LibFillResults, + LibAbiEncoder +{ + + function publicAbiEncodeFillOrder( + Order memory order, + uint256 takerAssetFillAmount, + bytes memory signature + ) + public + pure + returns (bytes memory fillOrderCalldata) + { + fillOrderCalldata = abiEncodeFillOrder( + order, + takerAssetFillAmount, + signature + ); + return fillOrderCalldata; + } + + function publicGetPartialAmount( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (uint256 partialAmount) + { + partialAmount = getPartialAmount( + numerator, + denominator, + target + ); + return partialAmount; + } + + function publicIsRoundingError( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (bool isError) + { + isError = isRoundingError( + numerator, + denominator, + target + ); + return isError; + } + + function publicGetOrderHash(Order memory order) + public + view + returns (bytes32 orderHash) + { + orderHash = getOrderHash(order); + return orderHash; + } + + function getOrderSchemaHash() + public + pure + returns (bytes32) + { + return EIP712_ORDER_SCHEMA_HASH; + } + + function getDomainSeparatorSchemaHash() + public + pure + returns (bytes32) + { + return EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH; + } + + function publicAddFillResults(FillResults memory totalFillResults, FillResults memory singleFillResults) + public + pure + returns (FillResults memory) + { + addFillResults(totalFillResults, singleFillResults); + return totalFillResults; + } +} diff --git a/packages/contracts/src/2.0.0/test/TestSignatureValidator/TestSignatureValidator.sol b/packages/contracts/src/2.0.0/test/TestSignatureValidator/TestSignatureValidator.sol new file mode 100644 index 000000000..e1a610469 --- /dev/null +++ b/packages/contracts/src/2.0.0/test/TestSignatureValidator/TestSignatureValidator.sol @@ -0,0 +1,46 @@ +/* + + 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/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol b/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol new file mode 100644 index 000000000..d9950145d --- /dev/null +++ b/packages/contracts/src/2.0.0/tokens/ERC20Token/ERC20Token.sol @@ -0,0 +1,96 @@ +/* + + 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 "./IERC20Token.sol"; + + +contract ERC20Token is IERC20Token { + + mapping (address => uint256) internal balances; + mapping (address => mapping (address => uint256)) internal allowed; + + uint256 public totalSupply; + + function transfer(address _to, uint256 _value) + public + returns (bool) + { + require( + balances[msg.sender] >= _value, + "ERC20_INSUFFICIENT_BALANCE" + ); + require( + balances[_to] + _value >= balances[_to], + "OVERFLOW" + ); + balances[msg.sender] -= _value; + balances[_to] += _value; + emit Transfer(msg.sender, _to, _value); + return true; + } + + function transferFrom(address _from, address _to, uint256 _value) + public + returns (bool) + { + require( + balances[_from] >= _value, + "ERC20_INSUFFICIENT_BALANCE" + ); + require( + allowed[_from][msg.sender] >= _value, + "ERC20_INSUFFICIENT_ALLOWANCE" + ); + require( + balances[_to] + _value >= balances[_to], + "OVERFLOW" + ); + balances[_to] += _value; + balances[_from] -= _value; + allowed[_from][msg.sender] -= _value; + emit Transfer(_from, _to, _value); + return true; + } + + function approve(address _spender, uint256 _value) + public + returns (bool) + { + allowed[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + function balanceOf(address _owner) + public + view + returns (uint256) + { + return balances[_owner]; + } + + function allowance(address _owner, address _spender) + public + view + returns (uint256) + { + return allowed[_owner][_spender]; + } +} diff --git a/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol b/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol new file mode 100644 index 000000000..5ee5e1011 --- /dev/null +++ b/packages/contracts/src/2.0.0/tokens/ERC20Token/IERC20Token.sol @@ -0,0 +1,74 @@ +/* + + 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; + + +contract IERC20Token { + + /// @notice send `value` token to `to` from `msg.sender` + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transfer(address _to, uint256 _value) + public + returns (bool); + + /// @notice send `value` token to `to` from `from` on the condition it is approved by `from` + /// @param _from The address of the sender + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transferFrom(address _from, address _to, uint256 _value) + public + returns (bool); + + /// @notice `msg.sender` approves `_spender` to spend `_value` tokens + /// @param _spender The address of the account able to transfer the tokens + /// @param _value The amount of wei to be approved for transfer + /// @return Whether the approval was successful or not + function approve(address _spender, uint256 _value) + public + returns (bool); + + /// @param _owner The address from which the balance will be retrieved + /// @return The balance + function balanceOf(address _owner) + public view + returns (uint256); + + /// @param _owner The address of the account owning tokens + /// @param _spender The address of the account able to transfer the tokens + /// @return Amount of remaining tokens allowed to spent + function allowance(address _owner, address _spender) + public view + returns (uint256); + + // solhint-disable-next-line no-simple-event-func-name + event Transfer( + address indexed _from, + address indexed _to, + uint256 _value + ); + + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _value + ); +} diff --git a/packages/contracts/src/2.0.0/tokens/ERC721Token/ERC721Token.sol b/packages/contracts/src/2.0.0/tokens/ERC721Token/ERC721Token.sol new file mode 100644 index 000000000..60603aa19 --- /dev/null +++ b/packages/contracts/src/2.0.0/tokens/ERC721Token/ERC721Token.sol @@ -0,0 +1,407 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016 Smart Contract Solutions, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +pragma solidity 0.4.24; + +import "./IERC721Token.sol"; +import "./IERC721Receiver.sol"; +import "../../utils/SafeMath/SafeMath.sol"; + + +/** + * @title ERC721 Non-Fungible Token Standard basic implementation + * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md + * Modified from https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/ERC721/ERC721BasicToken.sol + */ +contract ERC721Token is + IERC721Token, + SafeMath +{ + // Equals to `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))` + // which can be also obtained as `ERC721Receiver(0).onERC721Received.selector` + bytes4 constant internal ERC721_RECEIVED = 0xf0b9e5ba; + + // Mapping from token ID to owner + mapping (uint256 => address) internal tokenOwner; + + // Mapping from token ID to approved address + mapping (uint256 => address) internal tokenApprovals; + + // Mapping from owner to number of owned token + mapping (address => uint256) internal ownedTokensCount; + + // Mapping from owner to operator approvals + mapping (address => mapping (address => bool)) internal operatorApprovals; + + /** + * @dev Guarantees msg.sender is owner of the given token + * @param _tokenId uint256 ID of the token to validate its ownership belongs to msg.sender + */ + modifier onlyOwnerOf(uint256 _tokenId) { + require(ownerOf(_tokenId) == msg.sender); + _; + } + + /** + * @dev Checks msg.sender can transfer a token, by being owner, approved, or operator + * @param _tokenId uint256 ID of the token to validate + */ + modifier canTransfer(uint256 _tokenId) { + require(isApprovedOrOwner(msg.sender, _tokenId)); + _; + } + + constructor ( + string _name, + string _symbol) + public + { + name_ = _name; + symbol_ = _symbol; + } + + /** + * @dev Gets the token name + * @return string representing the token name + */ + function name() + public + view + returns (string) + { + return name_; + } + + /** + * @dev Gets the token symbol + * @return string representing the token symbol + */ + function symbol() + public + view + returns (string) + { + return symbol_; + } + + /** + * @dev Gets the balance of the specified address + * @param _owner address to query the balance of + * @return uint256 representing the amount owned by the passed address + */ + function balanceOf(address _owner) + public + view + returns (uint256) + { + require(_owner != address(0)); + return ownedTokensCount[_owner]; + } + + /** + * @dev Gets the owner of the specified token ID + * @param _tokenId uint256 ID of the token to query the owner of + * @return owner address currently marked as the owner of the given token ID + */ + function ownerOf(uint256 _tokenId) + public + view + returns (address) + { + address owner = tokenOwner[_tokenId]; + require(owner != address(0)); + return owner; + } + + /** + * @dev Returns whether the specified token exists + * @param _tokenId uint256 ID of the token to query the existance of + * @return whether the token exists + */ + function exists(uint256 _tokenId) + public + view + returns (bool) + { + address owner = tokenOwner[_tokenId]; + return owner != address(0); + } + + /** + * @dev Approves another address to transfer the given token ID + * @dev The zero address indicates there is no approved address. + * @dev There can only be one approved address per token at a given time. + * @dev Can only be called by the token owner or an approved operator. + * @param _to address to be approved for the given token ID + * @param _tokenId uint256 ID of the token to be approved + */ + function approve(address _to, uint256 _tokenId) + public + { + address owner = ownerOf(_tokenId); + require(_to != owner); + require(msg.sender == owner || isApprovedForAll(owner, msg.sender)); + + if (getApproved(_tokenId) != address(0) || _to != address(0)) { + tokenApprovals[_tokenId] = _to; + emit Approval(owner, _to, _tokenId); + } + } + + /** + * @dev Gets the approved address for a token ID, or zero if no address set + * @param _tokenId uint256 ID of the token to query the approval of + * @return address currently approved for a the given token ID + */ + function getApproved(uint256 _tokenId) + public + view + returns (address) + { + return tokenApprovals[_tokenId]; + } + + /** + * @dev Sets or unsets the approval of a given operator + * @dev An operator is allowed to transfer all tokens of the sender on their behalf + * @param _to operator address to set the approval + * @param _approved representing the status of the approval to be set + */ + function setApprovalForAll(address _to, bool _approved) + public + { + require(_to != msg.sender); + operatorApprovals[msg.sender][_to] = _approved; + emit ApprovalForAll(msg.sender, _to, _approved); + } + + /** + * @dev Tells whether an operator is approved by a given owner + * @param _owner owner address which you want to query the approval of + * @param _operator operator address which you want to query the approval of + * @return bool whether the given operator is approved by the given owner + */ + function isApprovedForAll(address _owner, address _operator) + public + view + returns (bool) + { + return operatorApprovals[_owner][_operator]; + } + + /** + * @dev Transfers the ownership of a given token ID to another address + * @dev Usage of this method is discouraged, use `safeTransferFrom` whenever possible + * @dev Requires the msg sender to be the owner, approved, or operator + * @param _from current owner of the token + * @param _to address to receive the ownership of the given token ID + * @param _tokenId uint256 ID of the token to be transferred + */ + function transferFrom(address _from, address _to, uint256 _tokenId) + public + canTransfer(_tokenId) + { + require(_from != address(0)); + require(_to != address(0)); + + clearApproval(_from, _tokenId); + removeTokenFrom(_from, _tokenId); + addTokenTo(_to, _tokenId); + + emit Transfer(_from, _to, _tokenId); + } + + /** + * @dev Safely transfers the ownership of a given token ID to another address + * @dev If the target address is a contract, it must implement `onERC721Received`, + * which is called upon a safe transfer, and return the magic value + * `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`; otherwise, + * the transfer is reverted. + * @dev Requires the msg sender to be the owner, approved, or operator + * @param _from current owner of the token + * @param _to address to receive the ownership of the given token ID + * @param _tokenId uint256 ID of the token to be transferred + */ + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId) + public + canTransfer(_tokenId) + { + // solium-disable-next-line arg-overflow + safeTransferFrom(_from, _to, _tokenId, ""); + } + + /** + * @dev Safely transfers the ownership of a given token ID to another address + * @dev If the target address is a contract, it must implement `onERC721Received`, + * which is called upon a safe transfer, and return the magic value + * `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`; otherwise, + * the transfer is reverted. + * @dev Requires the msg sender to be the owner, approved, or operator + * @param _from current owner of the token + * @param _to address to receive the ownership of the given token ID + * @param _tokenId uint256 ID of the token to be transferred + * @param _data bytes data to send along with a safe transfer check + */ + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId, + bytes _data) + public + canTransfer(_tokenId) + { + transferFrom(_from, _to, _tokenId); + // solium-disable-next-line arg-overflow + require(checkAndCallSafeTransfer(_from, _to, _tokenId, _data)); + } + + /** + * @dev Returns whether the given spender can transfer a given token ID + * @param _spender address of the spender to query + * @param _tokenId uint256 ID of the token to be transferred + * @return bool whether the msg.sender is approved for the given token ID, + * is an operator of the owner, or is the owner of the token + */ + function isApprovedOrOwner(address _spender, uint256 _tokenId) + internal + view + returns (bool) + { + address owner = ownerOf(_tokenId); + return _spender == owner || getApproved(_tokenId) == _spender || isApprovedForAll(owner, _spender); + } + + /** + * @dev Internal function to mint a new token + * @dev Reverts if the given token ID already exists + * @param _to The address that will own the minted token + * @param _tokenId uint256 ID of the token to be minted by the msg.sender + */ + function _mint(address _to, uint256 _tokenId) + internal + { + require(_to != address(0)); + addTokenTo(_to, _tokenId); + emit Transfer(address(0), _to, _tokenId); + } + + /** + * @dev Internal function to burn a specific token + * @dev Reverts if the token does not exist + * @param _tokenId uint256 ID of the token being burned by the msg.sender + */ + function _burn(address _owner, uint256 _tokenId) + internal + { + clearApproval(_owner, _tokenId); + removeTokenFrom(_owner, _tokenId); + emit Transfer(_owner, address(0), _tokenId); + } + + /** + * @dev Internal function to clear current approval of a given token ID + * @dev Reverts if the given address is not indeed the owner of the token + * @param _owner owner of the token + * @param _tokenId uint256 ID of the token to be transferred + */ + function clearApproval(address _owner, uint256 _tokenId) + internal + { + require(ownerOf(_tokenId) == _owner); + if (tokenApprovals[_tokenId] != address(0)) { + tokenApprovals[_tokenId] = address(0); + emit Approval(_owner, address(0), _tokenId); + } + } + + /** + * @dev Internal function to add a token ID to the list of a given address + * @param _to address representing the new owner of the given token ID + * @param _tokenId uint256 ID of the token to be added to the tokens list of the given address + */ + function addTokenTo(address _to, uint256 _tokenId) + internal + { + require(tokenOwner[_tokenId] == address(0)); + tokenOwner[_tokenId] = _to; + ownedTokensCount[_to] = safeAdd(ownedTokensCount[_to], 1); + } + + /** + * @dev Internal function to remove a token ID from the list of a given address + * @param _from address representing the previous owner of the given token ID + * @param _tokenId uint256 ID of the token to be removed from the tokens list of the given address + */ + function removeTokenFrom(address _from, uint256 _tokenId) + internal + { + require(ownerOf(_tokenId) == _from); + ownedTokensCount[_from] = safeSub(ownedTokensCount[_from], 1); + tokenOwner[_tokenId] = address(0); + } + + /** + * @dev Internal function to invoke `onERC721Received` on a target address + * @dev The call is not executed if the target address is not a contract + * @param _from address representing the previous owner of the given token ID + * @param _to target address that will receive the tokens + * @param _tokenId uint256 ID of the token to be transferred + * @param _data bytes optional data to send along with the call + * @return whether the call correctly returned the expected magic value + */ + function checkAndCallSafeTransfer( + address _from, + address _to, + uint256 _tokenId, + bytes _data) + internal + returns (bool) + { + if (!isContract(_to)) { + return true; + } + bytes4 retval = IERC721Receiver(_to).onERC721Received(_from, _tokenId, _data); + return (retval == ERC721_RECEIVED); + } + + function isContract(address addr) + internal + view + returns (bool) + { + uint256 size; + // XXX Currently there is no better way to check if there is a contract in an address + // than to check the size of the code at that address. + // See https://ethereum.stackexchange.com/a/14016/36603 + // for more details about how this works. + // TODO Check this again before the Serenity release, because all addresses will be + // contracts then. + assembly { size := extcodesize(addr) } // solium-disable-line security/no-inline-assembly + return size > 0; + } +} diff --git a/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Receiver.sol b/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Receiver.sol new file mode 100644 index 000000000..f2e8f3c88 --- /dev/null +++ b/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Receiver.sol @@ -0,0 +1,61 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016 Smart Contract Solutions, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +pragma solidity 0.4.24; + + +/** + * @title ERC721 token receiver interface + * @dev Interface for any contract that wants to support safeTransfers + * rom ERC721 asset contracts. + * Modified from https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/ERC721/ERC721Receiver.sol + */ +contract IERC721Receiver { + /** + * @dev Magic value to be returned upon successful reception of an NFT + * Equals to `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`, + * which can be also obtained as `ERC721Receiver(0).onERC721Received.selector` + */ + bytes4 constant internal ERC721_RECEIVED = 0xf0b9e5ba; + + /** + * @notice Handle the receipt of an NFT + * @dev The ERC721 smart contract calls this function on the recipient + * after a `safetransfer`. This function MAY throw to revert and reject the + * transfer. This function MUST use 50,000 gas or less. Return of other + * than the magic value MUST result in the transaction being reverted. + * Note: the contract address is always the message sender. + * @param _from The sending address + * @param _tokenId The NFT identifier which is being transfered + * @param _data Additional data with no specified format + * @return `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))` + */ + function onERC721Received( + address _from, + uint256 _tokenId, + bytes _data) + public + returns (bytes4); +} diff --git a/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Token.sol b/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Token.sol new file mode 100644 index 000000000..4d57ece38 --- /dev/null +++ b/packages/contracts/src/2.0.0/tokens/ERC721Token/IERC721Token.sol @@ -0,0 +1,118 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016 Smart Contract Solutions, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +pragma solidity 0.4.24; + + +/** + * @title ERC721 Non-Fungible Token Standard basic interface + * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md + * Modified from https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/ERC721/ERC721Basic.sol + */ +contract IERC721Token { + string internal name_; + string internal symbol_; + + event Transfer( + address indexed _from, + address indexed _to, + uint256 _tokenId + ); + + event Approval( + address indexed _owner, + address indexed _approved, + uint256 _tokenId + ); + + event ApprovalForAll( + address indexed _owner, + address indexed _operator, + bool _approved + ); + + function name() + public + view + returns (string); + + function symbol() + public + view + returns (string); + + function balanceOf(address _owner) + public + view + returns (uint256 _balance); + + function ownerOf(uint256 _tokenId) + public + view + returns (address _owner); + + function exists(uint256 _tokenId) + public + view + returns (bool _exists); + + function approve(address _to, uint256 _tokenId) + public; + + function getApproved(uint256 _tokenId) + public + view + returns (address _operator); + + function setApprovalForAll(address _operator, bool _approved) + public; + + function isApprovedForAll(address _owner, address _operator) + public + view + returns (bool); + + function transferFrom( + address _from, + address _to, + uint256 _tokenId + ) + public; + + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId + ) + public; + + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId, + bytes _data + ) + public; +} diff --git a/packages/contracts/src/2.0.0/tokens/EtherToken/IEtherToken.sol b/packages/contracts/src/2.0.0/tokens/EtherToken/IEtherToken.sol new file mode 100644 index 000000000..9e2e68766 --- /dev/null +++ b/packages/contracts/src/2.0.0/tokens/EtherToken/IEtherToken.sol @@ -0,0 +1,33 @@ +/* + + 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 "../ERC20Token/IERC20Token.sol"; + + +contract IEtherToken is + IERC20Token +{ + function deposit() + public + payable; + + function withdraw(uint256 amount) + public; +} diff --git a/packages/contracts/src/contracts/current/tokens/WETH9/WETH9.sol b/packages/contracts/src/2.0.0/tokens/EtherToken/WETH9.sol index 733ca414b..17876b86d 100644 --- a/packages/contracts/src/contracts/current/tokens/WETH9/WETH9.sol +++ b/packages/contracts/src/2.0.0/tokens/EtherToken/WETH9.sol @@ -13,17 +13,19 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. +// solhint-disable pragma solidity ^0.4.18; + contract WETH9 { string public name = "Wrapped Ether"; string public symbol = "WETH"; uint8 public decimals = 18; - event Approval(address indexed src, address indexed guy, uint wad); - event Transfer(address indexed src, address indexed dst, uint wad); - event Deposit(address indexed dst, uint wad); - event Withdrawal(address indexed src, uint wad); + event Approval(address indexed _owner, address indexed _spender, uint _value); + event Transfer(address indexed _from, address indexed _to, uint _value); + event Deposit(address indexed _owner, uint _value); + event Withdrawal(address indexed _owner, uint _value); mapping (address => uint) public balanceOf; mapping (address => mapping (address => uint)) public allowance; diff --git a/packages/contracts/src/contracts/current/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol b/packages/contracts/src/2.0.0/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol index 699f535d2..9feb5c914 100644 --- a/packages/contracts/src/contracts/current/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol +++ b/packages/contracts/src/2.0.0/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol @@ -1,6 +1,6 @@ /* - Copyright 2017 ZeroEx Intl. + 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. @@ -16,31 +16,43 @@ */ -pragma solidity ^0.4.18; +pragma solidity 0.4.24; + +import "../ERC20Token/ERC20Token.sol"; -import { ERC20Token } from "../ERC20Token/ERC20Token.sol"; contract UnlimitedAllowanceToken is ERC20Token { - uint constant MAX_UINT = 2**256 - 1; + uint256 constant internal MAX_UINT = 2**256 - 1; /// @dev ERC20 transferFrom, modified such that an allowance of MAX_UINT represents an unlimited allowance. See https://github.com/ethereum/EIPs/issues/717 /// @param _from Address to transfer from. /// @param _to Address to transfer to. /// @param _value Amount to transfer. /// @return Success of transfer. - function transferFrom(address _from, address _to, uint _value) + function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { - uint allowance = allowed[_from][msg.sender]; - require(balances[_from] >= _value && allowance >= _value && balances[_to] + _value >= balances[_to]); + uint256 allowance = allowed[_from][msg.sender]; + require( + balances[_from] >= _value, + "ERC20_INSUFFICIENT_BALANCE" + ); + require( + allowance >= _value, + "ERC20_INSUFFICIENT_ALLOWANCE" + ); + require( + balances[_to] + _value >= balances[_to], + "OVERFLOW" + ); balances[_to] += _value; balances[_from] -= _value; if (allowance < MAX_UINT) { allowed[_from][msg.sender] -= _value; } - Transfer(_from, _to, _value); + emit Transfer(_from, _to, _value); return true; } } diff --git a/packages/contracts/src/contracts/current/tokens/ZRXToken/ZRXToken.sol b/packages/contracts/src/2.0.0/tokens/ZRXToken/ZRXToken.sol index 7f5e1f849..28c0b2fb3 100644 --- a/packages/contracts/src/contracts/current/tokens/ZRXToken/ZRXToken.sol +++ b/packages/contracts/src/2.0.0/tokens/ZRXToken/ZRXToken.sol @@ -1,6 +1,6 @@ /* - Copyright 2017 ZeroEx Intl. + 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. @@ -16,18 +16,24 @@ */ -pragma solidity ^0.4.11; +pragma solidity 0.4.11; + +// solhint-disable-next-line max-line-length +import { UnlimitedAllowanceToken_v1 as UnlimitedAllowanceToken } from "../../../1.0.0/UnlimitedAllowanceToken/UnlimitedAllowanceToken_v1.sol"; -import { UnlimitedAllowanceToken_v1 as UnlimitedAllowanceToken } from "../../../previous/UnlimitedAllowanceToken/UnlimitedAllowanceToken_v1.sol"; contract ZRXToken is UnlimitedAllowanceToken { + // solhint-disable const-name-snakecase uint8 constant public decimals = 18; uint public totalSupply = 10**27; // 1 billion tokens, 18 decimal places string constant public name = "0x Protocol Token"; string constant public symbol = "ZRX"; + // solhint-enableconst-name-snakecase - function ZRXToken() { + function ZRXToken() + public + { balances[msg.sender] = totalSupply; } } diff --git a/packages/contracts/src/2.0.0/utils/LibBytes/LibBytes.sol b/packages/contracts/src/2.0.0/utils/LibBytes/LibBytes.sol new file mode 100644 index 000000000..504e950a8 --- /dev/null +++ b/packages/contracts/src/2.0.0/utils/LibBytes/LibBytes.sol @@ -0,0 +1,560 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + + +library LibBytes { + + using LibBytes for bytes; + + /// @dev Gets the memory address for a byte array. + /// @param input Byte array to lookup. + /// @return memoryAddress Memory address of byte array. This + /// points to the header of the byte array which contains + /// the length. + function rawAddress(bytes memory input) + internal + pure + returns (uint256 memoryAddress) + { + assembly { + memoryAddress := input + } + return memoryAddress; + } + + /// @dev Gets the memory address for the contents of a byte array. + /// @param input Byte array to lookup. + /// @return memoryAddress Memory address of the contents of the byte array. + function contentAddress(bytes memory input) + internal + pure + returns (uint256 memoryAddress) + { + assembly { + memoryAddress := add(input, 32) + } + return memoryAddress; + } + + /// @dev Copies `length` bytes from memory location `source` to `dest`. + /// @param dest memory address to copy bytes to. + /// @param source memory address to copy bytes from. + /// @param length number of bytes to copy. + function memCopy( + uint256 dest, + uint256 source, + uint256 length + ) + internal + pure + { + if (length < 32) { + // Handle a partial word by reading destination and masking + // off the bits we are interested in. + // This correctly handles overlap, zero lengths and source == dest + assembly { + let mask := sub(exp(256, sub(32, length)), 1) + let s := and(mload(source), not(mask)) + let d := and(mload(dest), mask) + mstore(dest, or(s, d)) + } + } else { + // Skip the O(length) loop when source == dest. + if (source == dest) { + return; + } + + // For large copies we copy whole words at a time. The final + // word is aligned to the end of the range (instead of after the + // previous) to handle partial words. So a copy will look like this: + // + // #### + // #### + // #### + // #### + // + // We handle overlap in the source and destination range by + // changing the copying direction. This prevents us from + // overwriting parts of source that we still need to copy. + // + // This correctly handles source == dest + // + if (source > dest) { + assembly { + // We subtract 32 from `sEnd` and `dEnd` because it + // is easier to compare with in the loop, and these + // are also the addresses we need for copying the + // last bytes. + length := sub(length, 32) + let sEnd := add(source, length) + let dEnd := add(dest, length) + + // Remember the last 32 bytes of source + // This needs to be done here and not after the loop + // because we may have overwritten the last bytes in + // source already due to overlap. + let last := mload(sEnd) + + // Copy whole words front to back + // Note: the first check is always true, + // this could have been a do-while loop. + // solhint-disable-next-line no-empty-blocks + for {} lt(source, sEnd) {} { + mstore(dest, mload(source)) + source := add(source, 32) + dest := add(dest, 32) + } + + // Write the last 32 bytes + mstore(dEnd, last) + } + } else { + assembly { + // We subtract 32 from `sEnd` and `dEnd` because those + // are the starting points when copying a word at the end. + length := sub(length, 32) + let sEnd := add(source, length) + let dEnd := add(dest, length) + + // Remember the first 32 bytes of source + // This needs to be done here and not after the loop + // because we may have overwritten the first bytes in + // source already due to overlap. + let first := mload(source) + + // Copy whole words back to front + // We use a signed comparisson here to allow dEnd to become + // negative (happens when source and dest < 32). Valid + // addresses in local memory will never be larger than + // 2**255, so they can be safely re-interpreted as signed. + // Note: the first check is always true, + // this could have been a do-while loop. + // solhint-disable-next-line no-empty-blocks + for {} slt(dest, dEnd) {} { + mstore(dEnd, mload(sEnd)) + sEnd := sub(sEnd, 32) + dEnd := sub(dEnd, 32) + } + + // Write the first 32 bytes + mstore(dest, first) + } + } + } + } + + /// @dev Returns a slices from a byte array. + /// @param b The byte array to take a slice from. + /// @param from The starting index for the slice (inclusive). + /// @param to The final index for the slice (exclusive). + /// @return result The slice containing bytes at indices [from, to) + function slice( + bytes memory b, + uint256 from, + uint256 to + ) + internal + pure + returns (bytes memory result) + { + require( + from <= to, + "FROM_LESS_THAN_TO_REQUIRED" + ); + require( + to < b.length, + "TO_LESS_THAN_LENGTH_REQUIRED" + ); + + // Create a new bytes structure and copy contents + result = new bytes(to - from); + memCopy( + result.contentAddress(), + b.contentAddress() + from, + result.length); + return result; + } + + /// @dev Returns a slice from a byte array without preserving the input. + /// @param b The byte array to take a slice from. Will be destroyed in the process. + /// @param from The starting index for the slice (inclusive). + /// @param to The final index for the slice (exclusive). + /// @return result The slice containing bytes at indices [from, to) + /// @dev When `from == 0`, the original array will match the slice. In other cases its state will be corrupted. + function sliceDestructive( + bytes memory b, + uint256 from, + uint256 to + ) + internal + pure + returns (bytes memory result) + { + require( + from <= to, + "FROM_LESS_THAN_TO_REQUIRED" + ); + require( + to < b.length, + "TO_LESS_THAN_LENGTH_REQUIRED" + ); + + // Create a new bytes structure around [from, to) in-place. + assembly { + result := add(b, from) + mstore(result, sub(to, from)) + } + return result; + } + + /// @dev Pops the last byte off of a byte array by modifying its length. + /// @param b Byte array that will be modified. + /// @return The byte that was popped off. + function popLastByte(bytes memory b) + internal + pure + returns (bytes1 result) + { + require( + b.length > 0, + "GREATER_THAN_ZERO_LENGTH_REQUIRED" + ); + + // Store last byte. + result = b[b.length - 1]; + + assembly { + // Decrement length of byte array. + let newLen := sub(mload(b), 1) + mstore(b, newLen) + } + return result; + } + + /// @dev Pops the last 20 bytes off of a byte array by modifying its length. + /// @param b Byte array that will be modified. + /// @return The 20 byte address that was popped off. + function popLast20Bytes(bytes memory b) + internal + pure + returns (address result) + { + require( + b.length >= 20, + "GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED" + ); + + // Store last 20 bytes. + result = readAddress(b, b.length - 20); + + assembly { + // Subtract 20 from byte array length. + let newLen := sub(mload(b), 20) + mstore(b, newLen) + } + return result; + } + + /// @dev Tests equality of two byte arrays. + /// @param lhs First byte array to compare. + /// @param rhs Second byte array to compare. + /// @return True if arrays are the same. False otherwise. + function equals( + bytes memory lhs, + bytes memory rhs + ) + internal + pure + returns (bool equal) + { + // Keccak gas cost is 30 + numWords * 6. This is a cheap way to compare. + // We early exit on unequal lengths, but keccak would also correctly + // handle this. + return lhs.length == rhs.length && keccak256(lhs) == keccak256(rhs); + } + + /// @dev Reads an address from a position in a byte array. + /// @param b Byte array containing an address. + /// @param index Index in byte array of address. + /// @return address from byte array. + function readAddress( + bytes memory b, + uint256 index + ) + internal + pure + returns (address result) + { + require( + b.length >= index + 20, // 20 is length of address + "GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED" + ); + + // Add offset to index: + // 1. Arrays are prefixed by 32-byte length parameter (add 32 to index) + // 2. Account for size difference between address length and 32-byte storage word (subtract 12 from index) + index += 20; + + // Read address from array memory + assembly { + // 1. Add index to address of bytes array + // 2. Load 32-byte word from memory + // 3. Apply 20-byte mask to obtain address + result := and(mload(add(b, index)), 0xffffffffffffffffffffffffffffffffffffffff) + } + return result; + } + + /// @dev Writes an address into a specific position in a byte array. + /// @param b Byte array to insert address into. + /// @param index Index in byte array of address. + /// @param input Address to put into byte array. + function writeAddress( + bytes memory b, + uint256 index, + address input + ) + internal + pure + { + require( + b.length >= index + 20, // 20 is length of address + "GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED" + ); + + // Add offset to index: + // 1. Arrays are prefixed by 32-byte length parameter (add 32 to index) + // 2. Account for size difference between address length and 32-byte storage word (subtract 12 from index) + index += 20; + + // Store address into array memory + assembly { + // The address occupies 20 bytes and mstore stores 32 bytes. + // First fetch the 32-byte word where we'll be storing the address, then + // apply a mask so we have only the bytes in the word that the address will not occupy. + // Then combine these bytes with the address and store the 32 bytes back to memory with mstore. + + // 1. Add index to address of bytes array + // 2. Load 32-byte word from memory + // 3. Apply 12-byte mask to obtain extra bytes occupying word of memory where we'll store the address + let neighbors := and( + mload(add(b, index)), + 0xffffffffffffffffffffffff0000000000000000000000000000000000000000 + ) + + // Make sure input address is clean. + // (Solidity does not guarantee this) + input := and(input, 0xffffffffffffffffffffffffffffffffffffffff) + + // Store the neighbors and address into memory + mstore(add(b, index), xor(input, neighbors)) + } + } + + /// @dev Reads a bytes32 value from a position in a byte array. + /// @param b Byte array containing a bytes32 value. + /// @param index Index in byte array of bytes32 value. + /// @return bytes32 value from byte array. + function readBytes32( + bytes memory b, + uint256 index + ) + internal + pure + returns (bytes32 result) + { + require( + b.length >= index + 32, + "GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED" + ); + + // Arrays are prefixed by a 256 bit length parameter + index += 32; + + // Read the bytes32 from array memory + assembly { + result := mload(add(b, index)) + } + return result; + } + + /// @dev Writes a bytes32 into a specific position in a byte array. + /// @param b Byte array to insert <input> into. + /// @param index Index in byte array of <input>. + /// @param input bytes32 to put into byte array. + function writeBytes32( + bytes memory b, + uint256 index, + bytes32 input + ) + internal + pure + { + require( + b.length >= index + 32, + "GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED" + ); + + // Arrays are prefixed by a 256 bit length parameter + index += 32; + + // Read the bytes32 from array memory + assembly { + mstore(add(b, index), input) + } + } + + /// @dev Reads a uint256 value from a position in a byte array. + /// @param b Byte array containing a uint256 value. + /// @param index Index in byte array of uint256 value. + /// @return uint256 value from byte array. + function readUint256( + bytes memory b, + uint256 index + ) + internal + pure + returns (uint256 result) + { + return uint256(readBytes32(b, index)); + } + + /// @dev Writes a uint256 into a specific position in a byte array. + /// @param b Byte array to insert <input> into. + /// @param index Index in byte array of <input>. + /// @param input uint256 to put into byte array. + function writeUint256( + bytes memory b, + uint256 index, + uint256 input + ) + internal + pure + { + writeBytes32(b, index, bytes32(input)); + } + + /// @dev Reads an unpadded bytes4 value from a position in a byte array. + /// @param b Byte array containing a bytes4 value. + /// @param index Index in byte array of bytes4 value. + /// @return bytes4 value from byte array. + function readBytes4( + bytes memory b, + uint256 index + ) + internal + pure + returns (bytes4 result) + { + require( + b.length >= index + 4, + "GREATER_OR_EQUAL_TO_4_LENGTH_REQUIRED" + ); + assembly { + result := mload(add(b, 32)) + // Solidity does not require us to clean the trailing bytes. + // We do it anyway + result := and(result, 0xFFFFFFFF00000000000000000000000000000000000000000000000000000000) + } + return result; + } + + /// @dev Reads nested bytes from a specific position. + /// @dev NOTE: the returned value overlaps with the input value. + /// Both should be treated as immutable. + /// @param b Byte array containing nested bytes. + /// @param index Index of nested bytes. + /// @return result Nested bytes. + function readBytesWithLength( + bytes memory b, + uint256 index + ) + internal + pure + returns (bytes memory result) + { + // Read length of nested bytes + uint256 nestedBytesLength = readUint256(b, index); + index += 32; + + // Assert length of <b> is valid, given + // length of nested bytes + require( + b.length >= index + nestedBytesLength, + "GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED" + ); + + // Return a pointer to the byte array as it exists inside `b` + assembly { + result := add(b, index) + } + return result; + } + + /// @dev Inserts bytes at a specific position in a byte array. + /// @param b Byte array to insert <input> into. + /// @param index Index in byte array of <input>. + /// @param input bytes to insert. + function writeBytesWithLength( + bytes memory b, + uint256 index, + bytes memory input + ) + internal + pure + { + // Assert length of <b> is valid, given + // length of input + require( + b.length >= index + 32 + input.length, // 32 bytes to store length + "GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED" + ); + + // Copy <input> into <b> + memCopy( + b.contentAddress() + index, + input.rawAddress(), // includes length of <input> + input.length + 32 // +32 bytes to store <input> length + ); + } + + /// @dev Performs a deep copy of a byte array onto another byte array of greater than or equal length. + /// @param dest Byte array that will be overwritten with source bytes. + /// @param source Byte array to copy onto dest bytes. + function deepCopyBytes( + bytes memory dest, + bytes memory source + ) + internal + pure + { + uint256 sourceLen = source.length; + // Dest length must be >= source length, or some bytes would not be copied. + require( + dest.length >= sourceLen, + "GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED" + ); + memCopy( + dest.contentAddress(), + source.contentAddress(), + sourceLen + ); + } +} diff --git a/packages/contracts/src/2.0.0/utils/Ownable/IOwnable.sol b/packages/contracts/src/2.0.0/utils/Ownable/IOwnable.sol new file mode 100644 index 000000000..116b8dc89 --- /dev/null +++ b/packages/contracts/src/2.0.0/utils/Ownable/IOwnable.sol @@ -0,0 +1,13 @@ +pragma solidity 0.4.24; + +/* + * Ownable + * + * Base contract with an owner. + * Provides onlyOwner modifier, which prevents function from running if it is called by anyone other than the owner. + */ + +contract IOwnable { + function transferOwnership(address newOwner) + public; +} diff --git a/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol b/packages/contracts/src/2.0.0/utils/Ownable/Ownable.sol index 9b3d6b9cf..aca65aad2 100644 --- a/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol +++ b/packages/contracts/src/2.0.0/utils/Ownable/Ownable.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.18; +pragma solidity 0.4.24; /* * Ownable @@ -7,17 +7,23 @@ pragma solidity ^0.4.18; * Provides onlyOwner modifier, which prevents function from running if it is called by anyone other than the owner. */ -contract Ownable { +import "./IOwnable.sol"; + + +contract Ownable is IOwnable { address public owner; - function Ownable() + constructor () public { owner = msg.sender; } modifier onlyOwner() { - require(msg.sender == owner); + require( + msg.sender == owner, + "ONLY_CONTRACT_OWNER" + ); _; } diff --git a/packages/contracts/src/contracts/current/utils/SafeMath/SafeMath.sol b/packages/contracts/src/2.0.0/utils/SafeMath/SafeMath.sol index 955a9e379..63a2a085f 100644 --- a/packages/contracts/src/contracts/current/utils/SafeMath/SafeMath.sol +++ b/packages/contracts/src/2.0.0/utils/SafeMath/SafeMath.sol @@ -1,41 +1,54 @@ -pragma solidity ^0.4.18; +pragma solidity 0.4.24; + contract SafeMath { - function safeMul(uint a, uint b) + function safeMul(uint256 a, uint256 b) internal pure returns (uint256) { - uint c = a * b; - assert(a == 0 || c / a == b); + if (a == 0) { + return 0; + } + uint256 c = a * b; + require( + c / a == b, + "UINT256_OVERFLOW" + ); return c; } - function safeDiv(uint a, uint b) + function safeDiv(uint256 a, uint256 b) internal pure returns (uint256) { - uint c = a / b; + uint256 c = a / b; return c; } - function safeSub(uint a, uint b) + function safeSub(uint256 a, uint256 b) internal pure returns (uint256) { - assert(b <= a); + require( + b <= a, + "UINT256_UNDERFLOW" + ); return a - b; } - function safeAdd(uint a, uint b) + function safeAdd(uint256 a, uint256 b) internal pure returns (uint256) { - uint c = a + b; - assert(c >= a); + uint256 c = a + b; + require( + c >= a, + "UINT256_OVERFLOW" + ); return c; } diff --git a/packages/contracts/src/contracts/current/test/DummyToken/DummyToken.sol b/packages/contracts/src/contracts/current/test/DummyToken/DummyToken.sol deleted file mode 100644 index ab04f4d16..000000000 --- a/packages/contracts/src/contracts/current/test/DummyToken/DummyToken.sol +++ /dev/null @@ -1,37 +0,0 @@ -pragma solidity ^0.4.18; - -import { Mintable } from "../Mintable/Mintable.sol"; -import { Ownable } from "../../utils/Ownable/Ownable.sol"; - -contract DummyToken is Mintable, Ownable { - string public name; - string public symbol; - uint public decimals; - - function DummyToken( - string _name, - string _symbol, - uint _decimals, - uint _totalSupply) - public - { - name = _name; - symbol = _symbol; - decimals = _decimals; - totalSupply = _totalSupply; - balances[msg.sender] = _totalSupply; - } - - function setBalance(address _target, uint _value) - public - onlyOwner - { - uint currBalance = balanceOf(_target); - if (_value < currBalance) { - totalSupply = safeSub(totalSupply, safeSub(currBalance, _value)); - } else { - totalSupply = safeAdd(totalSupply, safeSub(_value, currBalance)); - } - balances[_target] = _value; - } -} diff --git a/packages/contracts/src/contracts/current/test/MaliciousToken/MaliciousToken.sol b/packages/contracts/src/contracts/current/test/MaliciousToken/MaliciousToken.sol deleted file mode 100644 index 9e502616c..000000000 --- a/packages/contracts/src/contracts/current/test/MaliciousToken/MaliciousToken.sol +++ /dev/null @@ -1,31 +0,0 @@ -pragma solidity ^0.4.18; - -import { ERC20Token } from "../../tokens/ERC20Token/ERC20Token.sol"; - -contract MaliciousToken is ERC20Token { - uint8 stateToUpdate = 1; // Not null so that change only requires 5000 gas - - function updateState() - internal - { - stateToUpdate++; - } - - function balanceOf(address _owner) - public - constant - returns (uint) - { - updateState(); - return balances[_owner]; - } - - function allowance(address _owner, address _spender) - public - constant - returns (uint) - { - updateState(); - return allowed[_owner][_spender]; - } -} diff --git a/packages/contracts/src/contracts/current/test/Mintable/Mintable.sol b/packages/contracts/src/contracts/current/test/Mintable/Mintable.sol deleted file mode 100644 index cf7ee35a5..000000000 --- a/packages/contracts/src/contracts/current/test/Mintable/Mintable.sol +++ /dev/null @@ -1,18 +0,0 @@ -pragma solidity ^0.4.18; - -import { UnlimitedAllowanceToken } from "../../tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol"; -import { SafeMath } from "../../utils/SafeMath/SafeMath.sol"; - -/* - * Mintable - * Base contract that creates a mintable UnlimitedAllowanceToken - */ -contract Mintable is UnlimitedAllowanceToken, SafeMath { - function mint(uint _value) - public - { - require(_value <= 100000000000000000000); - balances[msg.sender] = safeAdd(_value, balances[msg.sender]); - totalSupply = safeAdd(totalSupply, _value); - } -} diff --git a/packages/contracts/src/contracts/current/tokens/ERC20Token/ERC20Token.sol b/packages/contracts/src/contracts/current/tokens/ERC20Token/ERC20Token.sol deleted file mode 100644 index 0e5b87aa4..000000000 --- a/packages/contracts/src/contracts/current/tokens/ERC20Token/ERC20Token.sol +++ /dev/null @@ -1,58 +0,0 @@ -pragma solidity ^0.4.18; - -import { Token } from "../Token/Token.sol"; - -contract ERC20Token is Token { - - function transfer(address _to, uint _value) - public - returns (bool) - { - require(balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]); - balances[msg.sender] -= _value; - balances[_to] += _value; - Transfer(msg.sender, _to, _value); - return true; - } - - function transferFrom(address _from, address _to, uint _value) - public - returns (bool) - { - require(balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value >= balances[_to]); - balances[_to] += _value; - balances[_from] -= _value; - allowed[_from][msg.sender] -= _value; - Transfer(_from, _to, _value); - return true; - } - - function approve(address _spender, uint _value) - public - returns (bool) - { - allowed[msg.sender][_spender] = _value; - Approval(msg.sender, _spender, _value); - return true; - } - - function balanceOf(address _owner) - public - view - returns (uint) - { - return balances[_owner]; - } - - function allowance(address _owner, address _spender) - public - view - returns (uint) - { - return allowed[_owner][_spender]; - } - - mapping (address => uint) balances; - mapping (address => mapping (address => uint)) allowed; - uint public totalSupply; -} diff --git a/packages/contracts/src/contracts/current/tokens/Token/Token.sol b/packages/contracts/src/contracts/current/tokens/Token/Token.sol deleted file mode 100644 index bf4e71dcd..000000000 --- a/packages/contracts/src/contracts/current/tokens/Token/Token.sol +++ /dev/null @@ -1,35 +0,0 @@ -pragma solidity ^0.4.18; - -contract Token { - - /// @notice send `_value` token to `_to` from `msg.sender` - /// @param _to The address of the recipient - /// @param _value The amount of token to be transferred - /// @return Whether the transfer was successful or not - function transfer(address _to, uint _value) public returns (bool) {} - - /// @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from` - /// @param _from The address of the sender - /// @param _to The address of the recipient - /// @param _value The amount of token to be transferred - /// @return Whether the transfer was successful or not - function transferFrom(address _from, address _to, uint _value) public returns (bool) {} - - /// @notice `msg.sender` approves `_addr` to spend `_value` tokens - /// @param _spender The address of the account able to transfer the tokens - /// @param _value The amount of wei to be approved for transfer - /// @return Whether the approval was successful or not - function approve(address _spender, uint _value) public returns (bool) {} - - /// @param _owner The address from which the balance will be retrieved - /// @return The balance - function balanceOf(address _owner) public view returns (uint) {} - - /// @param _owner The address of the account owning tokens - /// @param _spender The address of the account able to transfer the tokens - /// @return Amount of remaining tokens allowed to spent - function allowance(address _owner, address _spender) public view returns (uint) {} - - event Transfer(address indexed _from, address indexed _to, uint _value); - event Approval(address indexed _owner, address indexed _spender, uint _value); -} diff --git a/packages/contracts/test/asset_proxy/authorizable.ts b/packages/contracts/test/asset_proxy/authorizable.ts new file mode 100644 index 000000000..e99c6cee3 --- /dev/null +++ b/packages/contracts/test/asset_proxy/authorizable.ts @@ -0,0 +1,207 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; + +import { MixinAuthorizableContract } from '../../generated_contract_wrappers/mixin_authorizable'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +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 = accounts[0]; + notOwner = accounts[1]; + 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/packages/contracts/test/asset_proxy/proxies.ts b/packages/contracts/test/asset_proxy/proxies.ts new file mode 100644 index 000000000..62215f08d --- /dev/null +++ b/packages/contracts/test/asset_proxy/proxies.ts @@ -0,0 +1,386 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721ReceiverContract } from '../../generated_contract_wrappers/dummy_erc721_receiver'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; +import { IAssetProxyContract } from '../../generated_contract_wrappers/i_asset_proxy'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { LogDecoder } from '../utils/log_decoder'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const assetProxyInterface = new IAssetProxyContract( + artifacts.IAssetProxy.compilerOutput.abi, + constants.NULL_ADDRESS, + provider, +); + +// tslint:disable:no-unnecessary-type-assertion +describe('Asset Transfer Proxies', () => { + let owner: string; + let notAuthorized: string; + let exchangeAddress: string; + let makerAddress: string; + let takerAddress: string; + + let zrxToken: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; + let erc721Receiver: DummyERC721ReceiverContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; + + let erc20Wrapper: ERC20Wrapper; + let erc721Wrapper: ERC721Wrapper; + let erc721MakerTokenId: BigNumber; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, notAuthorized, exchangeAddress, makerAddress, takerAddress] = _.slice( + accounts, + 0, + 5, + )); + + 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(); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeAddress, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721MakerTokenId = erc721Balances[makerAddress][erc721Token.address][0]; + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeAddress, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + erc721Receiver = await DummyERC721ReceiverContract.deployFrom0xArtifactAsync( + artifacts.DummyERC721Receiver, + provider, + txDefaults, + ); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('Transfer Proxy - ERC20', () => { + describe('transferFrom', () => { + it('should successfully transfer tokens', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + // Perform a transfer from makerAddress to takerAddress + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: exchangeAddress, + }), + 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 do nothing if transferring 0 amount of a token', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + // Perform a transfer from makerAddress to takerAddress + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const amount = new BigNumber(0); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: exchangeAddress, + }), + 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], + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address], + ); + }); + + it('should throw if allowances are too low', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.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, + makerAddress, + takerAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(erc20Proxy.address, allowance, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Perform a transfer; expect this to fail. + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: exchangeAddress, + }), + RevertReason.TransferFailed, + ); + }); + + it('should throw if requesting address is not authorized', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: notAuthorized, + }), + RevertReason.SenderNotAuthorized, + ); + }); + }); + + it('should have an id of 0xf47261b0', async () => { + const proxyId = await erc20Proxy.getProxyId.callAsync(); + const expectedProxyId = '0xf47261b0'; + expect(proxyId).to.equal(expectedProxyId); + }); + }); + + describe('Transfer Proxy - ERC721', () => { + describe('transferFrom', () => { + it('should successfully transfer tokens', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Verify pre-condition + const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: exchangeAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress); + }); + + it('should not call onERC721Received when transferring to a smart contract', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Verify pre-condition + const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + erc721Receiver.address, + amount, + ); + const logDecoder = new LogDecoder(web3Wrapper, erc721Receiver.address); + const tx = await logDecoder.getTxWithDecodedLogsAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: exchangeAddress, + gas: constants.MAX_TRANSFER_FROM_GAS, + }), + ); + // Verify that no log was emitted by erc721 receiver + expect(tx.logs.length).to.be.equal(0); + // Verify transfer was successful + const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(newOwnerMakerAsset).to.be.bignumber.equal(erc721Receiver.address); + }); + + it('should throw if transferring 0 amount of a token', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Verify pre-condition + const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(0); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + return expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: exchangeAddress, + }), + RevertReason.InvalidAmount, + ); + }); + + it('should throw if transferring > 1 amount of a token', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Verify pre-condition + const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); + expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(500); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + return expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: exchangeAddress, + }), + RevertReason.InvalidAmount, + ); + }); + + it('should throw if allowances are too low', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Remove transfer approval for makerAddress. + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Token.approve.sendTransactionAsync(constants.NULL_ADDRESS, erc721MakerTokenId, { + from: makerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Perform a transfer; expect this to fail. + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + return expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: exchangeAddress, + }), + RevertReason.TransferFailed, + ); + }); + + it('should throw if requesting address is not authorized', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + // Perform a transfer from makerAddress to takerAddress + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + makerAddress, + takerAddress, + amount, + ); + return expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: notAuthorized, + }), + RevertReason.SenderNotAuthorized, + ); + }); + }); + + it('should have an id of 0x02571792', async () => { + const proxyId = await erc721Proxy.getProxyId.callAsync(); + const expectedProxyId = '0x02571792'; + expect(proxyId).to.equal(expectedProxyId); + }); + }); +}); +// tslint:enable:no-unnecessary-type-assertion +// tslint:disable:max-file-line-count diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index e276d11b3..b324988cc 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/packages/contracts/test/exchange/core.ts @@ -1,124 +1,125 @@ -import { LogWithDecodedArgs, SignedOrder, TransactionReceiptWithDecodedLogs, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils'; +import { RevertReason, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; import ethUtil = require('ethereumjs-util'); -import * as Web3 from 'web3'; - -import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token'; -import { - ExchangeContract, - LogCancelContractEventArgs, - LogErrorContractEventArgs, - LogFillContractEventArgs, -} from '../../src/contract_wrappers/generated/exchange'; -import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; -import { Balances } from '../../util/balances'; -import { constants } from '../../util/constants'; -import { crypto } from '../../util/crypto'; -import { ExchangeWrapper } from '../../util/exchange_wrapper'; -import { OrderFactory } from '../../util/order_factory'; -import { BalancesByOwner, ContractName, ExchangeContractErrs } from '../../util/types'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; +import { ExchangeCancelEventArgs, ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { getLatestBlockTimestampAsync, increaseTimeAndMineBlockAsync } from '../utils/block_timestamp'; import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { ERC20BalancesByOwner, OrderStatus } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('Exchange', () => { - let maker: string; - let tokenOwner: string; - let taker: string; - let feeRecipient: string; - const INITIAL_BALANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); - const INITIAL_ALLOWANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); - - let rep: DummyTokenContract; - let dgd: DummyTokenContract; - let zrx: DummyTokenContract; +// 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 exchange: ExchangeContract; - let tokenTransferProxy: TokenTransferProxyContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; let signedOrder: SignedOrder; - let balances: BalancesByOwner; - let exWrapper: ExchangeWrapper; - let dmyBalances: Balances; + let erc20Balances: ERC20BalancesByOwner; + let exchangeWrapper: ExchangeWrapper; + let erc20Wrapper: ERC20Wrapper; + let erc721Wrapper: ERC721Wrapper; let orderFactory: OrderFactory; - let zeroEx: ZeroEx; + 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(); - maker = accounts[0]; - [tokenOwner, taker, feeRecipient] = accounts; - const [repInstance, dgdInstance, zrxInstance] = await Promise.all([ - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - ]); - rep = new DummyTokenContract(web3Wrapper, repInstance.abi, repInstance.address); - dgd = new DummyTokenContract(web3Wrapper, dgdInstance.abi, dgdInstance.address); - zrx = new DummyTokenContract(web3Wrapper, zrxInstance.abi, zrxInstance.address); - const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); - tokenTransferProxy = new TokenTransferProxyContract( - web3Wrapper, - tokenTransferProxyInstance.abi, - tokenTransferProxyInstance.address, + 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, ); - const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ - zrx.address, - tokenTransferProxy.address, - ]); - exchange = new ExchangeContract(web3Wrapper, exchangeInstance.abi, exchangeInstance.address); - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] }); - zeroEx = new ZeroEx(web3.currentProvider, { - exchangeContractAddress: exchange.address, - networkId: constants.TESTRPC_NETWORK_ID, - }); - exWrapper = new ExchangeWrapper(exchange, zeroEx); + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address]; + erc721TakerAssetIds = erc721Balances[takerAddress][erc721Token.address]; + + 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); - const defaultOrderParams = { - exchangeContractAddress: exchange.address, - maker, - feeRecipient, - makerTokenAddress: rep.address, - takerTokenAddress: dgd.address, - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), - makerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), - takerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), - }; - orderFactory = new OrderFactory(zeroEx, defaultOrderParams); - dmyBalances = new Balances([rep, dgd, zrx], [maker, taker, feeRecipient]); - await Promise.all([ - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: maker, - }), - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, - }), - rep.setBalance.sendTransactionAsync(maker, INITIAL_BALANCE, { from: tokenOwner }), - rep.setBalance.sendTransactionAsync(taker, INITIAL_BALANCE, { from: tokenOwner }), - dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: maker, - }), - dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, }), - dgd.setBalance.sendTransactionAsync(maker, INITIAL_BALANCE, { from: tokenOwner }), - dgd.setBalance.sendTransactionAsync(taker, INITIAL_BALANCE, { from: tokenOwner }), - zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: maker, - }), - zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, }), - zrx.setBalance.sendTransactionAsync(maker, INITIAL_BALANCE, { from: tokenOwner }), - zrx.setBalance.sendTransactionAsync(taker, INITIAL_BALANCE, { from: tokenOwner }), - ]); + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + 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(); @@ -126,756 +127,454 @@ describe('Exchange', () => { afterEach(async () => { await blockchainLifecycle.revertAsync(); }); - describe('internal functions', () => { - it('should include transferViaTokenTransferProxy', () => { - expect((exchange as any).transferViaTokenTransferProxy).to.be.undefined(); - }); - - it('should include isTransferable', () => { - expect((exchange as any).isTransferable).to.be.undefined(); - }); - - it('should include getBalance', () => { - expect((exchange as any).getBalance).to.be.undefined(); - }); - - it('should include getAllowance', () => { - expect((exchange as any).getAllowance).to.be.undefined(); - }); - }); - describe('fillOrder', () => { beforeEach(async () => { - balances = await dmyBalances.getAsync(); + erc20Balances = await erc20Wrapper.getBalancesAsync(); signedOrder = await orderFactory.newSignedOrderAsync(); }); - it('should create an unfillable order', async () => { + it('should throw if signature is invalid', async () => { signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: new BigNumber(1001), - takerTokenAmount: new BigNumber(3), - }); - - const filledTakerTokenAmountBefore = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountBefore).to.be.bignumber.equal(0); - - const fillTakerTokenAmount1 = new BigNumber(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: fillTakerTokenAmount1, - }); - - const filledTakerTokenAmountAfter1 = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountAfter1).to.be.bignumber.equal(fillTakerTokenAmount1); - - const fillTakerTokenAmount2 = new BigNumber(1); - await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: fillTakerTokenAmount2, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), }); - const filledTakerTokenAmountAfter2 = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), + 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, ); - expect(filledTakerTokenAmountAfter2).to.be.bignumber.equal(filledTakerTokenAmountAfter1); }); - it('should transfer the correct amounts when makerTokenAmount === takerTokenAmount', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - }); - - const filledTakerTokenAmountBefore = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountBefore).to.be.bignumber.equal(0); - - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { fillTakerTokenAmount }); - - const filledTakerTokenAmountAfter = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountAfter).to.be.bignumber.equal(fillTakerTokenAmount); - - const newBalances = await dmyBalances.getAsync(); - - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); - const paidMakerFee = signedOrder.makerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - const paidTakerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(fillMakerTokenAmount), - ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(fillTakerTokenAmount), - ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(paidMakerFee), - ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(fillTakerTokenAmount), - ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(fillMakerTokenAmount), - ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(paidTakerFee), - ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(paidMakerFee.add(paidTakerFee)), + 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 transfer the correct amounts when makerTokenAmount > takerTokenAmount', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - }); - - const filledTakerTokenAmountBefore = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountBefore).to.be.bignumber.equal(0); - - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { fillTakerTokenAmount }); + describe('cancelOrder', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + signedOrder = await orderFactory.newSignedOrderAsync(); + }); - const filledTakerTokenAmountAfter = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountAfter).to.be.bignumber.equal(fillTakerTokenAmount); - - const newBalances = await dmyBalances.getAsync(); - - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); - const paidMakerFee = signedOrder.makerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - const paidTakerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(fillMakerTokenAmount), - ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(fillTakerTokenAmount), - ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(paidMakerFee), - ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(fillTakerTokenAmount), - ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(fillMakerTokenAmount), - ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(paidTakerFee), - ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(paidMakerFee.add(paidTakerFee)), + it('should throw if not sent by maker', async () => { + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrderAsync(signedOrder, takerAddress), + RevertReason.InvalidMaker, ); }); - it('should transfer the correct amounts when makerTokenAmount < takerTokenAmount', async () => { + it('should throw if makerAssetAmount is 0', async () => { signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), + makerAssetAmount: new BigNumber(0), }); - const filledTakerTokenAmountBefore = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountBefore).to.be.bignumber.equal(0); - - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { fillTakerTokenAmount }); - - const filledTakerTokenAmountAfter = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountAfter).to.be.bignumber.equal(fillTakerTokenAmount); - - const newBalances = await dmyBalances.getAsync(); - - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); - const paidMakerFee = signedOrder.makerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - const paidTakerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(fillMakerTokenAmount), - ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(fillTakerTokenAmount), - ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(paidMakerFee), - ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(fillTakerTokenAmount), - ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(fillMakerTokenAmount), - ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(paidTakerFee), - ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(paidMakerFee.add(paidTakerFee)), + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress), + RevertReason.OrderUnfillable, ); }); - it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => { + it('should throw if takerAssetAmount is 0', async () => { signedOrder = await orderFactory.newSignedOrderAsync({ - taker, - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), + takerAssetAmount: new BigNumber(0), }); - const filledTakerTokenAmountBefore = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - expect(filledTakerTokenAmountBefore).to.be.bignumber.equal(0); - - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { fillTakerTokenAmount }); - - const filledTakerTokenAmountAfter = await zeroEx.exchange.getFilledTakerAmountAsync( - ZeroEx.getOrderHashHex(signedOrder), - ); - const expectedFillAmountTAfter = fillTakerTokenAmount.add(filledTakerTokenAmountBefore); - expect(filledTakerTokenAmountAfter).to.be.bignumber.equal(expectedFillAmountTAfter); - - const newBalances = await dmyBalances.getAsync(); - - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); - const paidMakerFee = signedOrder.makerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - const paidTakerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(fillMakerTokenAmount), - ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(fillTakerTokenAmount), - ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(paidMakerFee), - ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(fillTakerTokenAmount), - ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(fillMakerTokenAmount), - ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(paidTakerFee), - ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(paidMakerFee.add(paidTakerFee)), + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress), + RevertReason.OrderUnfillable, ); }); - it('should fill remaining value if fillTakerTokenAmount > remaining takerTokenAmount', async () => { - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrderAsync(signedOrder, taker, { fillTakerTokenAmount }); - - const res = await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: signedOrder.takerTokenAmount, - }); - const log = res.logs[0] as LogWithDecodedArgs<LogFillContractEventArgs>; - expect(log.args.filledTakerTokenAmount).to.be.bignumber.equal( - signedOrder.takerTokenAmount.minus(fillTakerTokenAmount), - ); - const newBalances = await dmyBalances.getAsync(); - - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(signedOrder.makerTokenAmount), - ); - expect(newBalances[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(signedOrder.takerTokenAmount), - ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(signedOrder.makerFee), - ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(signedOrder.takerTokenAmount), - ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(signedOrder.makerTokenAmount), - ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(signedOrder.takerFee), - ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(signedOrder.makerFee.add(signedOrder.takerFee)), + 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 the correct arguments when order has a feeRecipient', async () => { - const divisor = 2; - const res = await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: signedOrder.takerTokenAmount.div(divisor), - }); - expect(res.logs).to.have.length(1); - - const logArgs = (res.logs[0] as LogWithDecodedArgs<LogFillContractEventArgs>).args; - const expectedFilledMakerTokenAmount = signedOrder.makerTokenAmount.div(divisor); - const expectedFilledTakerTokenAmount = signedOrder.takerTokenAmount.div(divisor); - const expectedFeeMPaid = signedOrder.makerFee.div(divisor); - const expectedFeeTPaid = signedOrder.takerFee.div(divisor); - const tokensHashBuff = crypto.solSHA3([signedOrder.makerTokenAddress, signedOrder.takerTokenAddress]); - const expectedTokens = ethUtil.bufferToHex(tokensHashBuff); - - expect(signedOrder.maker).to.be.equal(logArgs.maker); - expect(taker).to.be.equal(logArgs.taker); - expect(signedOrder.feeRecipient).to.be.equal(logArgs.feeRecipient); - expect(signedOrder.makerTokenAddress).to.be.equal(logArgs.makerToken); - expect(signedOrder.takerTokenAddress).to.be.equal(logArgs.takerToken); - expect(expectedFilledMakerTokenAmount).to.be.bignumber.equal(logArgs.filledMakerTokenAmount); - expect(expectedFilledTakerTokenAmount).to.be.bignumber.equal(logArgs.filledTakerTokenAmount); - expect(expectedFeeMPaid).to.be.bignumber.equal(logArgs.paidMakerFee); - expect(expectedFeeTPaid).to.be.bignumber.equal(logArgs.paidTakerFee); - expect(expectedTokens).to.be.equal(logArgs.tokens); - expect(ZeroEx.getOrderHashHex(signedOrder)).to.be.equal(logArgs.orderHash); - }); - - it('should log 1 event with the correct arguments when order has no feeRecipient', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - feeRecipient: ZeroEx.NULL_ADDRESS, - }); - const divisor = 2; - const res = await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: signedOrder.takerTokenAmount.div(divisor), - }); + it('should log 1 event with correct arguments', async () => { + const res = await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress); expect(res.logs).to.have.length(1); - const logArgs = (res.logs[0] as LogWithDecodedArgs<LogFillContractEventArgs>).args; - const expectedFilledMakerTokenAmount = signedOrder.makerTokenAmount.div(divisor); - const expectedFilledTakerTokenAmount = signedOrder.takerTokenAmount.div(divisor); - const expectedFeeMPaid = new BigNumber(0); - const expectedFeeTPaid = new BigNumber(0); - const tokensHashBuff = crypto.solSHA3([signedOrder.makerTokenAddress, signedOrder.takerTokenAddress]); - const expectedTokens = ethUtil.bufferToHex(tokensHashBuff); - - expect(signedOrder.maker).to.be.equal(logArgs.maker); - expect(taker).to.be.equal(logArgs.taker); - expect(signedOrder.feeRecipient).to.be.equal(logArgs.feeRecipient); - expect(signedOrder.makerTokenAddress).to.be.equal(logArgs.makerToken); - expect(signedOrder.takerTokenAddress).to.be.equal(logArgs.takerToken); - expect(expectedFilledMakerTokenAmount).to.be.bignumber.equal(logArgs.filledMakerTokenAmount); - expect(expectedFilledTakerTokenAmount).to.be.bignumber.equal(logArgs.filledTakerTokenAmount); - expect(expectedFeeMPaid).to.be.bignumber.equal(logArgs.paidMakerFee); - expect(expectedFeeTPaid).to.be.bignumber.equal(logArgs.paidTakerFee); - expect(expectedTokens).to.be.equal(logArgs.tokens); - expect(ZeroEx.getOrderHashHex(signedOrder)).to.be.equal(logArgs.orderHash); - }); - - it('should throw when taker is specified and order is claimed by other', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - taker: feeRecipient, - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), - }); - - return expect(exWrapper.fillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if signature is invalid', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), - }); + const log = res.logs[0] as LogWithDecodedArgs<ExchangeCancelEventArgs>; + const logArgs = log.args; - signedOrder.ecSignature.r = ethUtil.bufferToHex(ethUtil.sha3('invalidR')); - signedOrder.ecSignature.s = ethUtil.bufferToHex(ethUtil.sha3('invalidS')); - return expect(exWrapper.fillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); + 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 makerTokenAmount is 0', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: new BigNumber(0), - }); - - return expect(exWrapper.fillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); + it('should throw if already cancelled', async () => { + await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress); + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress), + RevertReason.OrderUnfillable, + ); }); - it('should throw if takerTokenAmount is 0', async () => { + it('should throw if order is expired', async () => { + const currentTimestamp = await getLatestBlockTimestampAsync(); signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAmount: new BigNumber(0), + expirationTimeSeconds: new BigNumber(currentTimestamp).sub(10), }); - - return expect(exWrapper.fillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if fillTakerTokenAmount is 0', async () => { - signedOrder = await orderFactory.newSignedOrderAsync(); - - return expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: new BigNumber(0), - }), - ).to.be.rejectedWith(constants.REVERT); + return expectTransactionFailedAsync( + exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress), + RevertReason.OrderUnfillable, + ); }); - it('should not change balances if maker balances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { + it('should throw if rounding error is greater than 0.1%', async () => { signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100000), 18), + makerAssetAmount: new BigNumber(1001), + takerAssetAmount: new BigNumber(3), }); - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should throw if maker balances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = true', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100000), 18), + const fillTakerAssetAmount1 = new BigNumber(2); + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { + takerAssetFillAmount: fillTakerAssetAmount1, }); - return expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - shouldThrowOnInsufficientBalanceOrAllowance: true, + const fillTakerAssetAmount2 = new BigNumber(1); + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { + takerAssetFillAmount: fillTakerAssetAmount2, }), - ).to.be.rejectedWith(constants.REVERT); + RevertReason.RoundingError, + ); }); + }); - it('should not change balances if taker balances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100000), 18), - }); - - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); + 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 throw if taker balances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = true', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100000), 18), - }); - - return expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - shouldThrowOnInsufficientBalanceOrAllowance: true, - }), - ).to.be.rejectedWith(constants.REVERT); + 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 not change balances if maker allowances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - await rep.approve.sendTransactionAsync(tokenTransferProxy.address, new BigNumber(0), { from: maker }); - await exWrapper.fillOrderAsync(signedOrder, taker); - await rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: maker, - }); - - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); + 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); - it('should throw if maker allowances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = true', async () => { - await rep.approve.sendTransactionAsync(tokenTransferProxy.address, new BigNumber(0), { from: maker }); - expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - shouldThrowOnInsufficientBalanceOrAllowance: true, + // 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), }), - ).to.be.rejectedWith(constants.REVERT); - await rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: maker, - }); - }); - - it('should not change balances if taker allowances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - await dgd.approve.sendTransactionAsync(tokenTransferProxy.address, new BigNumber(0), { from: taker }); - await exWrapper.fillOrderAsync(signedOrder, taker); - await dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, - }); - - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should throw if taker allowances are too low to fill order and \ - shouldThrowOnInsufficientBalanceOrAllowance = true', async () => { - await dgd.approve.sendTransactionAsync(tokenTransferProxy.address, new BigNumber(0), { from: taker }); - expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - shouldThrowOnInsufficientBalanceOrAllowance: true, + await orderFactory.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(79), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(79), 18), + salt: new BigNumber(1), }), - ).to.be.rejectedWith(constants.REVERT); - await dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, - }); - }); - - it('should not change balances if makerTokenAddress is ZRX, makerTokenAmount + makerFee > maker balance, \ - and shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - const makerZRXBalance = new BigNumber(balances[maker][zrx.address]); - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAddress: zrx.address, - makerTokenAmount: makerZRXBalance, - makerFee: new BigNumber(1), - }); - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should not change balances if makerTokenAddress is ZRX, makerTokenAmount + makerFee > maker allowance, \ - and shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - const makerZRXAllowance = await zrx.allowance.callAsync(maker, tokenTransferProxy.address); - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAddress: zrx.address, - makerTokenAmount: new BigNumber(makerZRXAllowance), - makerFee: new BigNumber(1), - }); - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should not change balances if takerTokenAddress is ZRX, takerTokenAmount + takerFee > taker balance, \ - and shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - const takerZRXBalance = new BigNumber(balances[taker][zrx.address]); - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAddress: zrx.address, - takerTokenAmount: takerZRXBalance, - takerFee: new BigNumber(1), - }); - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should not change balances if takerTokenAddress is ZRX, takerTokenAmount + takerFee > taker allowance, \ - and shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - const takerZRXAllowance = await zrx.allowance.callAsync(taker, tokenTransferProxy.address); - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAddress: zrx.address, - takerTokenAmount: new BigNumber(takerZRXAllowance), - takerFee: new BigNumber(1), - }); - await exWrapper.fillOrderAsync(signedOrder, taker); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should throw if getBalance or getAllowance attempts to change state and \ - shouldThrowOnInsufficientBalanceOrAllowance = false', async () => { - const maliciousToken = await deployer.deployAsync(ContractName.MaliciousToken); - await maliciousToken.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { - from: taker, - }); - - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAddress: maliciousToken.address, - }); - - return expect( - exWrapper.fillOrderAsync(signedOrder, taker, { - shouldThrowOnInsufficientBalanceOrAllowance: false, + await orderFactory.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(979), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(979), 18), + salt: new BigNumber(2), }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should not change balances if an order is expired', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - expirationUnixTimestampSec: new BigNumber(Math.floor((Date.now() - 10000) / 1000)), - }); - await exWrapper.fillOrderAsync(signedOrder, taker); - - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should log an error event if an order is expired', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - expirationUnixTimestampSec: new BigNumber(Math.floor((Date.now() - 10000) / 1000)), - }); - - const res = await exWrapper.fillOrderAsync(signedOrder, taker); - expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<LogErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_EXPIRED); - }); - - it('should log an error event if no value is filled', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({}); - await exWrapper.fillOrderAsync(signedOrder, taker); - - const res = await exWrapper.fillOrderAsync(signedOrder, taker); - expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<LogErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_FULLY_FILLED_OR_CANCELLED); - }); - }); - - describe('cancelOrder', () => { - beforeEach(async () => { - balances = await dmyBalances.getAsync(); - signedOrder = await orderFactory.newSignedOrderAsync(); - }); - - it('should throw if not sent by maker', async () => { - return expect(exWrapper.cancelOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if makerTokenAmount is 0', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: new BigNumber(0), - }); - - return expect(exWrapper.cancelOrderAsync(signedOrder, maker)).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if takerTokenAmount is 0', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - takerTokenAmount: new BigNumber(0), - }); - - return expect(exWrapper.cancelOrderAsync(signedOrder, maker)).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if cancelTakerTokenAmount is 0', async () => { - signedOrder = await orderFactory.newSignedOrderAsync(); - - return expect( - exWrapper.cancelOrderAsync(signedOrder, maker, { - cancelTakerTokenAmount: new BigNumber(0), + await orderFactory.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(7979), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(7979), 18), + salt: new BigNumber(3), }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should be able to cancel a full order', async () => { - await exWrapper.cancelOrderAsync(signedOrder, maker); - await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: signedOrder.takerTokenAmount.div(2), - }); - - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); - }); - - it('should be able to cancel part of an order', async () => { - const cancelTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.cancelOrderAsync(signedOrder, maker, { - cancelTakerTokenAmount, - }); - - const res = await exWrapper.fillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount: signedOrder.takerTokenAmount, + ]; + 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: 490000, }); - const log = res.logs[0] as LogWithDecodedArgs<LogFillContractEventArgs>; - expect(log.args.filledTakerTokenAmount).to.be.bignumber.equal( - signedOrder.takerTokenAmount.minus(cancelTakerTokenAmount), - ); - const newBalances = await dmyBalances.getAsync(); - const cancelMakerTokenAmount = cancelTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); - const paidMakerFee = signedOrder.makerFee - .times(cancelMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - const paidTakerFee = signedOrder.takerFee - .times(cancelMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(cancelMakerTokenAmount), + 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[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(cancelTakerTokenAmount), + expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerAssetAddress].add(fillTakerAssetAmount), ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(paidMakerFee), + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFee), ); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(cancelTakerTokenAmount), + expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerAssetAddress].minus(fillTakerAssetAmount), ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(cancelMakerTokenAmount), + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].add(fillMakerAssetAmount), ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(paidTakerFee), + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFee), ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(paidMakerFee.add(paidTakerFee)), + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFee.add(takerFee)), ); }); + }); - it('should log 1 event with correct arguments', async () => { - const divisor = 2; - const res = await exWrapper.cancelOrderAsync(signedOrder, maker, { - cancelTakerTokenAmount: signedOrder.takerTokenAmount.div(divisor), + 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), }); - expect(res.logs).to.have.length(1); - - const log = res.logs[0] as LogWithDecodedArgs<LogCancelContractEventArgs>; - const logArgs = log.args; - const expectedCancelledMakerTokenAmount = signedOrder.makerTokenAmount.div(divisor); - const expectedCancelledTakerTokenAmount = signedOrder.takerTokenAmount.div(divisor); - const tokensHashBuff = crypto.solSHA3([signedOrder.makerTokenAddress, signedOrder.takerTokenAddress]); - const expectedTokens = ethUtil.bufferToHex(tokensHashBuff); - - expect(signedOrder.maker).to.be.equal(logArgs.maker); - expect(signedOrder.feeRecipient).to.be.equal(logArgs.feeRecipient); - expect(signedOrder.makerTokenAddress).to.be.equal(logArgs.makerToken); - expect(signedOrder.takerTokenAddress).to.be.equal(logArgs.takerToken); - expect(expectedCancelledMakerTokenAmount).to.be.bignumber.equal(logArgs.cancelledMakerTokenAmount); - expect(expectedCancelledTakerTokenAmount).to.be.bignumber.equal(logArgs.cancelledTakerTokenAmount); - expect(expectedTokens).to.be.equal(logArgs.tokens); - expect(ZeroEx.getOrderHashHex(signedOrder)).to.be.equal(logArgs.orderHash); + // Call Exchange + const takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + return expectTransactionFailedAsync( + exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { takerAssetFillAmount }), + RevertReason.RoundingError, + ); }); + }); - it('should not log events if no value is cancelled', async () => { - await exWrapper.cancelOrderAsync(signedOrder, maker); - - const res = await exWrapper.cancelOrderAsync(signedOrder, maker); - expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<LogErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_FULLY_FILLED_OR_CANCELLED); + describe('getOrderInfo', () => { + beforeEach(async () => { + signedOrder = await orderFactory.newSignedOrderAsync(); }); - - it('should not log events if order is expired', async () => { - signedOrder = await orderFactory.newSignedOrderAsync({ - expirationUnixTimestampSec: new BigNumber(Math.floor((Date.now() - 10000) / 1000)), - }); - - const res = await exWrapper.cancelOrderAsync(signedOrder, maker); - expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<LogErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_EXPIRED); + 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-line:max-file-line-count +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/exchange/dispatcher.ts b/packages/contracts/test/exchange/dispatcher.ts new file mode 100644 index 000000000..81d142ca4 --- /dev/null +++ b/packages/contracts/test/exchange/dispatcher.ts @@ -0,0 +1,226 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { AssetProxyId, RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; +import { + TestAssetProxyDispatcherAssetProxyRegisteredEventArgs, + TestAssetProxyDispatcherContract, +} from '../../generated_contract_wrappers/test_asset_proxy_dispatcher'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { LogDecoder } from '../utils/log_decoder'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_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, assetProxyDispatcher.address); + const txReceipt = await logDecoder.getTxWithDecodedLogsAsync( + await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }), + ); + const logs = txReceipt.logs; + const log = logs[0] as LogWithDecodedArgs<TestAssetProxyDispatcherAssetProxyRegisteredEventArgs>; + expect(log.args.id).to.equal(AssetProxyId.ERC20); + expect(log.args.assetProxy).to.equal(erc20Proxy.address); + }); + }); + + describe('getAssetProxy', () => { + it('should return correct address of registered proxy', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(erc20Proxy.address); + }); + + it('should return NULL address if requesting non-existent proxy', async () => { + const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(constants.NULL_ADDRESS); + }); + }); + + describe('dispatchTransferFrom', () => { + it('should dispatch transfer to registered proxy', async () => { + // Register ERC20 proxy + await web3Wrapper.awaitTransactionSuccessAsync( + await assetProxyDispatcher.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Construct metadata for ERC20 proxy + const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + + // Perform a transfer from makerAddress to takerAddress + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const amount = new BigNumber(10); + await web3Wrapper.awaitTransactionSuccessAsync( + await assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync( + encodedAssetData, + makerAddress, + takerAddress, + amount, + { from: owner }, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(amount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].add(amount), + ); + }); + + it('should 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/packages/contracts/test/exchange/fill_order.ts b/packages/contracts/test/exchange/fill_order.ts new file mode 100644 index 000000000..1494fe093 --- /dev/null +++ b/packages/contracts/test/exchange/fill_order.ts @@ -0,0 +1,301 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import * as _ from 'lodash'; + +import { chaiSetup } from '../utils/chai_setup'; +import { + FillOrderCombinatorialUtils, + fillOrderCombinatorialUtilsFactoryAsync, +} from '../utils/fill_order_combinatorial_utils'; +import { + AllowanceAmountScenario, + AssetDataScenario, + BalanceAmountScenario, + ExpirationTimeSecondsScenario, + FeeRecipientAddressScenario, + FillScenario, + OrderAssetAmountScenario, + TakerAssetFillAmountScenario, + TakerScenario, +} from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +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 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/packages/contracts/test/exchange/helpers.ts b/packages/contracts/test/exchange/helpers.ts deleted file mode 100644 index 080cbe5fd..000000000 --- a/packages/contracts/test/exchange/helpers.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { SignedOrder, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import ethUtil = require('ethereumjs-util'); - -import { - ExchangeContract, - LogCancelContractEventArgs, - LogErrorContractEventArgs, - LogFillContractEventArgs, -} from '../../src/contract_wrappers/generated/exchange'; -import { constants } from '../../util/constants'; -import { ExchangeWrapper } from '../../util/exchange_wrapper'; -import { OrderFactory } from '../../util/order_factory'; -import { ContractName } from '../../util/types'; -import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; - -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('Exchange', () => { - let maker: string; - let feeRecipient: string; - - let signedOrder: SignedOrder; - let exchangeWrapper: ExchangeWrapper; - let orderFactory: OrderFactory; - - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - [maker, feeRecipient] = accounts; - const tokenRegistry = await deployer.deployAsync(ContractName.TokenRegistry); - const tokenTransferProxy = await deployer.deployAsync(ContractName.TokenTransferProxy); - const [rep, dgd, zrx] = await Promise.all([ - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - ]); - const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ - zrx.address, - tokenTransferProxy.address, - ]); - const exchange = new ExchangeContract(web3Wrapper, exchangeInstance.abi, exchangeInstance.address); - await tokenTransferProxy.addAuthorizedAddress(exchange.address, { from: accounts[0] }); - const zeroEx = new ZeroEx(web3.currentProvider, { networkId: constants.TESTRPC_NETWORK_ID }); - exchangeWrapper = new ExchangeWrapper(exchange, zeroEx); - const defaultOrderParams = { - exchangeContractAddress: exchange.address, - maker, - feeRecipient, - makerTokenAddress: rep.address, - takerTokenAddress: dgd.address, - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), - makerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), - takerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), - }; - orderFactory = new OrderFactory(zeroEx, defaultOrderParams); - signedOrder = await orderFactory.newSignedOrderAsync(); - }); - - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('getOrderHash', () => { - it('should output the correct orderHash', async () => { - const orderHashHex = await exchangeWrapper.getOrderHashAsync(signedOrder); - expect(ZeroEx.getOrderHashHex(signedOrder)).to.be.equal(orderHashHex); - }); - }); - - describe('isValidSignature', () => { - beforeEach(async () => { - signedOrder = await orderFactory.newSignedOrderAsync(); - }); - - it('should return true with a valid signature', async () => { - const success = await exchangeWrapper.isValidSignatureAsync(signedOrder); - const orderHashHex = ZeroEx.getOrderHashHex(signedOrder); - const isValidSignature = ZeroEx.isValidSignature(orderHashHex, signedOrder.ecSignature, signedOrder.maker); - expect(isValidSignature).to.be.true(); - expect(success).to.be.true(); - }); - - it('should return false with an invalid signature', async () => { - signedOrder.ecSignature.r = ethUtil.bufferToHex(ethUtil.sha3('invalidR')); - signedOrder.ecSignature.s = ethUtil.bufferToHex(ethUtil.sha3('invalidS')); - const success = await exchangeWrapper.isValidSignatureAsync(signedOrder); - const orderHashHex = ZeroEx.getOrderHashHex(signedOrder); - const isValidSignature = ZeroEx.isValidSignature(orderHashHex, signedOrder.ecSignature, signedOrder.maker); - expect(isValidSignature).to.be.false(); - expect(success).to.be.false(); - }); - }); - - describe('isRoundingError', () => { - it('should return false if there is a rounding error of 0.1%', async () => { - const numerator = new BigNumber(20); - const denominator = new BigNumber(999); - const target = new BigNumber(50); - // rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1% - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - - it('should return false if there is a rounding of 0.09%', async () => { - const numerator = new BigNumber(20); - const denominator = new BigNumber(9991); - const target = new BigNumber(500); - // rounding error = ((20*500/9991) - floor(20*500/9991)) / (20*500/9991) = 0.09% - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - - it('should return true if there is a rounding error of 0.11%', async () => { - const numerator = new BigNumber(20); - const denominator = new BigNumber(9989); - const target = new BigNumber(500); - // rounding error = ((20*500/9989) - floor(20*500/9989)) / (20*500/9989) = 0.011% - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.true(); - }); - - it('should return true if there is a rounding error > 0.1%', async () => { - const numerator = new BigNumber(3); - const denominator = new BigNumber(7); - const target = new BigNumber(10); - // rounding error = ((3*10/7) - floor(3*10/7)) / (3*10/7) = 6.67% - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.true(); - }); - - it('should return false when there is no rounding error', async () => { - const numerator = new BigNumber(1); - const denominator = new BigNumber(2); - const target = new BigNumber(10); - - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - - it('should return false when there is rounding error <= 0.1%', async () => { - // randomly generated numbers - const numerator = new BigNumber(76564); - const denominator = new BigNumber(676373677); - const target = new BigNumber(105762562); - // rounding error = ((76564*105762562/676373677) - floor(76564*105762562/676373677)) / - // (76564*105762562/676373677) = 0.0007% - const isRoundingError = await exchangeWrapper.isRoundingErrorAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - }); - - describe('getPartialAmount', () => { - it('should return the numerator/denominator*target', async () => { - const numerator = new BigNumber(1); - const denominator = new BigNumber(2); - const target = new BigNumber(10); - - const partialAmount = await exchangeWrapper.getPartialAmountAsync(numerator, denominator, target); - const expectedPartialAmount = 5; - expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount); - }); - - it('should round down', async () => { - const numerator = new BigNumber(2); - const denominator = new BigNumber(3); - const target = new BigNumber(10); - - const partialAmount = await exchangeWrapper.getPartialAmountAsync(numerator, denominator, target); - const expectedPartialAmount = 6; - expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount); - }); - - it('should round .5 down', async () => { - const numerator = new BigNumber(1); - const denominator = new BigNumber(20); - const target = new BigNumber(10); - - const partialAmount = await exchangeWrapper.getPartialAmountAsync(numerator, denominator, target); - const expectedPartialAmount = 0; - expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount); - }); - }); -}); diff --git a/packages/contracts/test/exchange/internal.ts b/packages/contracts/test/exchange/internal.ts new file mode 100644 index 000000000..67d1d2d2c --- /dev/null +++ b/packages/contracts/test/exchange/internal.ts @@ -0,0 +1,305 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { Order, RevertReason, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { TestExchangeInternalsContract } from '../../generated_contract_wrappers/test_exchange_internals'; +import { artifacts } from '../utils/artifacts'; +import { + getInvalidOpcodeErrorMessageForCallAsync, + getRevertReasonOrErrorMessageForSendTransactionAsync, +} from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { bytes32Values, testCombinatoriallyWithReferenceFuncAsync, uint256Values } from '../utils/combinatorial_utils'; +import { constants } from '../utils/constants'; +import { FillResults } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +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); + +async function referenceGetPartialAmountAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): Promise<BigNumber> { + const invalidOpcodeErrorForCall = new Error(await getInvalidOpcodeErrorMessageForCallAsync()); + const product = numerator.mul(target); + if (product.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + if (denominator.eq(0)) { + throw invalidOpcodeErrorForCall; + } + return product.dividedToIntegerBy(denominator); +} + +describe('Exchange core internal functions', () => { + let testExchange: TestExchangeInternalsContract; + let invalidOpcodeErrorForCall: Error | undefined; + let overflowErrorForSendTransaction: 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), + ); + invalidOpcodeErrorForCall = new Error(await getInvalidOpcodeErrorMessageForCallAsync()); + }); + // Note(albrow): Don't forget to add beforeEach and afterEach calls to reset + // the blockchain state for any tests which modify it! + + describe('addFillResults', async () => { + function makeFillResults(value: BigNumber): FillResults { + return { + makerAssetFilledAmount: value, + takerAssetFilledAmount: value, + makerFeePaid: value, + takerFeePaid: value, + }; + } + async function referenceAddFillResultsAsync( + totalValue: BigNumber, + singleValue: BigNumber, + ): Promise<FillResults> { + // Note(albrow): Here, each of totalFillResults and + // singleFillResults will consist of fields with the same values. + // This should be safe because none of the fields in a given + // FillResults are ever used together in a mathemetical operation. + // They are only used with the corresponding field from *the other* + // FillResults, which are different. + const totalFillResults = makeFillResults(totalValue); + const singleFillResults = makeFillResults(singleValue); + // HACK(albrow): _.mergeWith mutates the first argument! To + // workaround this we use _.cloneDeep. + return _.mergeWith( + _.cloneDeep(totalFillResults), + singleFillResults, + (totalVal: BigNumber, singleVal: BigNumber) => { + const newTotal = totalVal.add(singleVal); + if (newTotal.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + return newTotal; + }, + ); + } + async function testAddFillResultsAsync(totalValue: BigNumber, singleValue: BigNumber): Promise<FillResults> { + const totalFillResults = makeFillResults(totalValue); + const singleFillResults = makeFillResults(singleValue); + return testExchange.publicAddFillResults.callAsync(totalFillResults, singleFillResults); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'addFillResults', + referenceAddFillResultsAsync, + testAddFillResultsAsync, + [uint256Values, uint256Values], + ); + }); + + describe('calculateFillResults', async () => { + function makeOrder( + makerAssetAmount: BigNumber, + takerAssetAmount: BigNumber, + makerFee: BigNumber, + takerFee: BigNumber, + ): Order { + return { + ...emptyOrder, + makerAssetAmount, + takerAssetAmount, + makerFee, + takerFee, + }; + } + async function referenceCalculateFillResultsAsync( + orderTakerAssetAmount: BigNumber, + takerAssetFilledAmount: BigNumber, + otherAmount: BigNumber, + ): Promise<FillResults> { + // Note(albrow): Here we are re-using the same value (otherAmount) + // for order.makerAssetAmount, order.makerFee, and order.takerFee. + // This should be safe because they are never used with each other + // in any mathematical operation in either the reference TypeScript + // implementation or the Solidity implementation of + // calculateFillResults. + return { + makerAssetFilledAmount: await referenceGetPartialAmountAsync( + takerAssetFilledAmount, + orderTakerAssetAmount, + otherAmount, + ), + takerAssetFilledAmount, + makerFeePaid: await referenceGetPartialAmountAsync( + takerAssetFilledAmount, + orderTakerAssetAmount, + otherAmount, + ), + takerFeePaid: await referenceGetPartialAmountAsync( + takerAssetFilledAmount, + orderTakerAssetAmount, + otherAmount, + ), + }; + } + async function testCalculateFillResultsAsync( + orderTakerAssetAmount: BigNumber, + takerAssetFilledAmount: BigNumber, + otherAmount: BigNumber, + ): Promise<FillResults> { + const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); + return testExchange.publicCalculateFillResults.callAsync(order, takerAssetFilledAmount); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'calculateFillResults', + referenceCalculateFillResultsAsync, + testCalculateFillResultsAsync, + [uint256Values, uint256Values, uint256Values], + ); + }); + + describe('getPartialAmount', async () => { + async function testGetPartialAmountAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, + ): Promise<BigNumber> { + return testExchange.publicGetPartialAmount.callAsync(numerator, denominator, target); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'getPartialAmount', + referenceGetPartialAmountAsync, + testGetPartialAmountAsync, + [uint256Values, uint256Values, uint256Values], + ); + }); + + describe('isRoundingError', async () => { + async function referenceIsRoundingErrorAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, + ): Promise<boolean> { + const product = numerator.mul(target); + if (denominator.eq(0)) { + throw invalidOpcodeErrorForCall; + } + const remainder = product.mod(denominator); + if (remainder.eq(0)) { + return false; + } + if (product.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + if (product.eq(0)) { + throw invalidOpcodeErrorForCall; + } + const remainderTimes1000000 = remainder.mul('1000000'); + if (remainderTimes1000000.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + const errPercentageTimes1000000 = remainderTimes1000000.dividedToIntegerBy(product); + return errPercentageTimes1000000.greaterThan('1000'); + } + async function testIsRoundingErrorAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, + ): Promise<boolean> { + return testExchange.publicIsRoundingError.callAsync(numerator, denominator, target); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'isRoundingError', + referenceIsRoundingErrorAsync, + testIsRoundingErrorAsync, + [uint256Values, uint256Values, uint256Values], + ); + }); + + describe('updateFilledState', async () => { + // Note(albrow): Since updateFilledState modifies the state by calling + // sendTransaction, we must reset the state after each test. + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + async function referenceUpdateFilledStateAsync( + takerAssetFilledAmount: BigNumber, + orderTakerAssetFilledAmount: BigNumber, + // tslint:disable-next-line:no-unused-variable + orderHash: string, + ): Promise<BigNumber> { + const totalFilledAmount = takerAssetFilledAmount.add(orderTakerAssetFilledAmount); + if (totalFilledAmount.greaterThan(MAX_UINT256)) { + throw overflowErrorForSendTransaction; + } + return totalFilledAmount; + } + async function testUpdateFilledStateAsync( + takerAssetFilledAmount: BigNumber, + orderTakerAssetFilledAmount: BigNumber, + orderHash: string, + ): Promise<BigNumber> { + const fillResults = { + makerAssetFilledAmount: new BigNumber(0), + takerAssetFilledAmount, + makerFeePaid: new BigNumber(0), + takerFeePaid: new BigNumber(0), + }; + await web3Wrapper.awaitTransactionSuccessAsync( + await testExchange.publicUpdateFilledState.sendTransactionAsync( + emptySignedOrder, + constants.NULL_ADDRESS, + orderHash, + orderTakerAssetFilledAmount, + fillResults, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + return testExchange.filled.callAsync(orderHash); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'updateFilledState', + referenceUpdateFilledStateAsync, + testUpdateFilledStateAsync, + [uint256Values, uint256Values, bytes32Values], + ); + }); +}); diff --git a/packages/contracts/test/exchange/libs.ts b/packages/contracts/test/exchange/libs.ts new file mode 100644 index 000000000..5c9f9aac7 --- /dev/null +++ b/packages/contracts/test/exchange/libs.ts @@ -0,0 +1,125 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils, EIP712Utils, orderHashUtils } from '@0xproject/order-utils'; +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; + +import { TestConstantsContract } from '../../generated_contract_wrappers/test_constants'; +import { TestLibsContract } from '../../generated_contract_wrappers/test_libs'; +import { addressUtils } from '../utils/address_utils'; +import { artifacts } from '../utils/artifacts'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { OrderFactory } from '../utils/order_factory'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; + +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('Exchange libs', () => { + let signedOrder: SignedOrder; + let orderFactory: OrderFactory; + let libs: TestLibsContract; + let testConstants: TestConstantsContract; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const makerAddress = accounts[0]; + libs = await TestLibsContract.deployFrom0xArtifactAsync(artifacts.TestLibs, provider, txDefaults); + testConstants = await TestConstantsContract.deployFrom0xArtifactAsync( + artifacts.TestConstants, + provider, + txDefaults, + ); + + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + exchangeAddress: libs.address, + makerAddress, + feeRecipientAddress: addressUtils.generatePseudoRandomAddress(), + makerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()), + takerAssetData: assetDataUtils.encodeERC20AssetData(addressUtils.generatePseudoRandomAddress()), + }; + 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('LibConstants', () => { + describe('ZRX_ASSET_DATA', () => { + it('should have the correct ZRX_ASSET_DATA', async () => { + const isValid = await testConstants.assertValidZrxAssetData.callAsync(); + expect(isValid).to.be.equal(true); + }); + }); + }); + // Note(albrow): These tests are designed to be supplemental to the + // combinatorial tests in test/exchange/internal. They test specific edge + // cases that are not covered by the combinatorial tests. + describe('LibMath', () => { + it('should return false if there is a rounding error of 0.1%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(999); + const target = new BigNumber(50); + // rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1% + const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); + expect(isRoundingError).to.be.false(); + }); + it('should return false if there is a rounding of 0.09%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(9991); + const target = new BigNumber(500); + // rounding error = ((20*500/9991) - floor(20*500/9991)) / (20*500/9991) = 0.09% + const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); + expect(isRoundingError).to.be.false(); + }); + it('should return true if there is a rounding error of 0.11%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(9989); + const target = new BigNumber(500); + // rounding error = ((20*500/9989) - floor(20*500/9989)) / (20*500/9989) = 0.011% + const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); + expect(isRoundingError).to.be.true(); + }); + }); + + describe('LibOrder', () => { + describe('getOrderSchema', () => { + it('should output the correct order schema hash', async () => { + const orderSchema = await libs.getOrderSchemaHash.callAsync(); + const schemaHashBuffer = orderHashUtils._getOrderSchemaBuffer(); + const schemaHashHex = `0x${schemaHashBuffer.toString('hex')}`; + expect(schemaHashHex).to.be.equal(orderSchema); + }); + }); + describe('getDomainSeparatorSchema', () => { + it('should output the correct domain separator schema hash', async () => { + const domainSeparatorSchema = await libs.getDomainSeparatorSchemaHash.callAsync(); + const domainSchemaBuffer = EIP712Utils._getDomainSeparatorSchemaBuffer(); + const schemaHashHex = `0x${domainSchemaBuffer.toString('hex')}`; + expect(schemaHashHex).to.be.equal(domainSeparatorSchema); + }); + }); + describe('getOrderHash', () => { + it('should output the correct orderHash', async () => { + signedOrder = await orderFactory.newSignedOrderAsync(); + const orderHashHex = await libs.publicGetOrderHash.callAsync(signedOrder); + expect(orderHashUtils.getOrderHashHex(signedOrder)).to.be.equal(orderHashHex); + }); + }); + }); +}); diff --git a/packages/contracts/test/exchange/match_orders.ts b/packages/contracts/test/exchange/match_orders.ts new file mode 100644 index 000000000..46b3569bd --- /dev/null +++ b/packages/contracts/test/exchange/match_orders.ts @@ -0,0 +1,796 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +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'; +import { OrderFactory } from '../utils/order_factory'; +import { ERC20BalancesByOwner, ERC721TokenIdsByOwner, OrderInfo, OrderStatus } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +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 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; + + 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, + ); + // Set default addresses + defaultERC20MakerAssetAddress = erc20TokenA.address; + defaultERC20TakerAssetAddress = erc20TokenB.address; + defaultERC721AssetAddress = erc721Token.address; + // Create default order parameters + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + exchangeAddress: exchange.address, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + }; + const privateKeyLeft = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressLeft)]; + orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParams); + const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)]; + orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParams); + // Set match order tester + matchOrderTester = new MatchOrderTester(exchangeWrapper, erc20Wrapper, erc721Wrapper, zrxToken.address); + }); + 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 the correct amounts when orders completely fill each other', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + }); + + 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({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Store original taker balance + const takerInitialBalances = _.cloneDeep(erc20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress]); + // Match signedOrderLeft with signedOrderRight + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + // prettier-ignore + [ + newERC20BalancesByOwner, + // tslint:disable-next-line:trailing-comma + newERC721TokenIdsByOwner + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify taker did not take a profit + expect(takerInitialBalances).to.be.deep.equal( + newERC20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress], + ); + }); + + 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({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(20), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was partially filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + + 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({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + }); + + 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({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + // prettier-ignore + [ + newERC20BalancesByOwner, + // tslint:disable-next-line:trailing-comma + newERC721TokenIdsByOwner + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // 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({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match signedOrderLeft with signedOrderRight2 + const leftTakerAssetFilledAmount = signedOrderRight.makerAssetAmount; + const rightTakerAssetFilledAmount = new BigNumber(0); + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight2, + takerAddress, + newERC20BalancesByOwner, + erc721TokenIdsByOwner, + leftTakerAssetFilledAmount, + rightTakerAssetFilledAmount, + ); + // Verify left order was fully filled + const leftOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo2.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify second right order was partially filled + const rightOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight2); + expect(rightOrderInfo2.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + + 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({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + // prettier-ignore + [ + newERC20BalancesByOwner, + // tslint:disable-next-line:trailing-comma + newERC721TokenIdsByOwner + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + // 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({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + // Match signedOrderLeft2 with signedOrderRight + const leftTakerAssetFilledAmount = new BigNumber(0); + const takerAmountReceived = newERC20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress].minus( + erc20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress], + ); + const rightTakerAssetFilledAmount = signedOrderLeft.makerAssetAmount.minus(takerAmountReceived); + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft2, + signedOrderRight, + takerAddress, + newERC20BalancesByOwner, + erc721TokenIdsByOwner, + leftTakerAssetFilledAmount, + rightTakerAssetFilledAmount, + ); + // Verify second left order was partially filled + const leftOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft2); + expect(leftOrderInfo2.orderStatus as OrderStatus).to.be.equal(OrderStatus.FILLABLE); + // Verify right order was fully filled + const rightOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo2.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + }); + + 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({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the left order maker', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = signedOrderLeft.makerAddress; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the right order maker', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = signedOrderRight.makerAddress; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the left fee recipient', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = feeRecipientAddressLeft; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the right fee recipient', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = feeRecipientAddressRight; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + 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({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: makerAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: makerAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('Should throw if left order is not fillable', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // 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({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // 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({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // 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({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // 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({ + makerAddress: makerAddressLeft, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // 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({ + makerAddress: makerAddressLeft, + makerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + makerAssetAmount: new BigNumber(1), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: new BigNumber(1), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + }); + + 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({ + makerAddress: makerAddressLeft, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: new BigNumber(1), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC721AssetData(defaultERC721AssetAddress, erc721TokenToTransfer), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: new BigNumber(1), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as OrderStatus).to.be.equal(OrderStatus.FULLY_FILLED); + }); + }); +}); // tslint:disable-line:max-file-line-count diff --git a/packages/contracts/test/exchange/signature_validator.ts b/packages/contracts/test/exchange/signature_validator.ts new file mode 100644 index 000000000..56a06c247 --- /dev/null +++ b/packages/contracts/test/exchange/signature_validator.ts @@ -0,0 +1,507 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { addSignedMessagePrefix, assetDataUtils, orderHashUtils } from '@0xproject/order-utils'; +import { RevertReason, SignatureType, SignedOrder, SignerType } from '@0xproject/types'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; +import ethUtil = require('ethereumjs-util'); + +import { + TestSignatureValidatorContract, + TestSignatureValidatorSignatureValidatorApprovalEventArgs, +} from '../../generated_contract_wrappers/test_signature_validator'; +import { ValidatorContract } from '../../generated_contract_wrappers/validator'; +import { WalletContract } from '../../generated_contract_wrappers/wallet'; +import { addressUtils } from '../utils/address_utils'; +import { artifacts } from '../utils/artifacts'; +import { expectContractCallFailed } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { LogDecoder } from '../utils/log_decoder'; +import { OrderFactory } from '../utils/order_factory'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +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 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( + artifacts.Wallet, + provider, + txDefaults, + signerAddress, + ); + testValidator = await ValidatorContract.deployFrom0xArtifactAsync( + artifacts.Validator, + provider, + txDefaults, + signerAddress, + ); + signatureValidatorLogDecoder = new LogDecoder(web3Wrapper, signatureValidator.address); + await web3Wrapper.awaitTransactionSuccessAsync( + await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync(testValidator.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 expectContractCallFailed( + 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 expectContractCallFailed( + 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 expectContractCallFailed( + 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 expectContractCallFailed( + 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 = addSignedMessagePrefix(orderHashHex, SignerType.Default); + 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 = addSignedMessagePrefix(orderHashHex, SignerType.Default); + 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=Caller and signer is caller', async () => { + const signature = ethUtil.toBuffer(`0x${SignatureType.Caller}`); + const signatureHex = ethUtil.bufferToHex(signature); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signerAddress, + signatureHex, + { from: signerAddress }, + ); + expect(isValidSignature).to.be.true(); + }); + + it('should return false when SignatureType=Caller and signer is not caller', async () => { + const signature = ethUtil.toBuffer(`0x${SignatureType.Caller}`); + const signatureHex = ethUtil.bufferToHex(signature); + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const isValidSignature = await signatureValidator.publicIsValidSignature.callAsync( + orderHashHex, + signerAddress, + signatureHex, + { from: notSignerAddress }, + ); + 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 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 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=Trezor and signature is valid', async () => { + // Create Trezor signature + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const orderHashWithTrezorPrefixHex = addSignedMessagePrefix(orderHashHex, SignerType.Trezor); + const orderHashWithTrezorPrefixBuffer = ethUtil.toBuffer(orderHashWithTrezorPrefixHex); + const ecSignature = ethUtil.ecsign(orderHashWithTrezorPrefixBuffer, signerPrivateKey); + // Create 0x signature from Trezor signature + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(`0x${SignatureType.Trezor}`), + ]); + 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=Trezor and signature is invalid', async () => { + // Create Trezor signature + const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); + const orderHashWithTrezorPrefixHex = addSignedMessagePrefix(orderHashHex, SignerType.Trezor); + const orderHashWithTrezorPrefixBuffer = ethUtil.toBuffer(orderHashWithTrezorPrefixHex); + const ecSignature = ethUtil.ecsign(orderHashWithTrezorPrefixBuffer, signerPrivateKey); + // Create 0x signature from Trezor signature + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(`0x${SignatureType.Trezor}`), + ]); + 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=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(); + }); + }); + + describe('setSignatureValidatorApproval', () => { + it('should emit a SignatureValidatorApprovalSet with correct args when a validator is approved', async () => { + const approval = true; + const res = await signatureValidatorLogDecoder.getTxWithDecodedLogsAsync( + await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync( + testValidator.address, + approval, + { + from: signerAddress, + }, + ), + ); + expect(res.logs.length).to.equal(1); + const log = res.logs[0] as LogWithDecodedArgs<TestSignatureValidatorSignatureValidatorApprovalEventArgs>; + const logArgs = log.args; + expect(logArgs.signerAddress).to.equal(signerAddress); + expect(logArgs.validatorAddress).to.equal(testValidator.address); + expect(logArgs.approved).to.equal(approval); + }); + it('should emit a SignatureValidatorApprovalSet with correct args when a validator is disapproved', async () => { + const approval = false; + const res = await signatureValidatorLogDecoder.getTxWithDecodedLogsAsync( + await signatureValidator.setSignatureValidatorApproval.sendTransactionAsync( + testValidator.address, + approval, + { + from: signerAddress, + }, + ), + ); + expect(res.logs.length).to.equal(1); + const log = res.logs[0] as LogWithDecodedArgs<TestSignatureValidatorSignatureValidatorApprovalEventArgs>; + const logArgs = log.args; + expect(logArgs.signerAddress).to.equal(signerAddress); + expect(logArgs.validatorAddress).to.equal(testValidator.address); + expect(logArgs.approved).to.equal(approval); + }); + }); +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/exchange/transactions.ts b/packages/contracts/test/exchange/transactions.ts new file mode 100644 index 000000000..2bdd96b16 --- /dev/null +++ b/packages/contracts/test/exchange/transactions.ts @@ -0,0 +1,462 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils, generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { OrderWithoutExchangeAddress, RevertReason, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { ExchangeWrapperContract } from '../../generated_contract_wrappers/exchange_wrapper'; +import { WhitelistContract } from '../../generated_contract_wrappers/whitelist'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { orderUtils } from '../utils/order_utils'; +import { TransactionFactory } from '../utils/transaction_factory'; +import { ERC20BalancesByOwner, SignedTransaction } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_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( + artifacts.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( + artifacts.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/packages/contracts/test/exchange/wrapper.ts b/packages/contracts/test/exchange/wrapper.ts index 0d3e18711..d48441dca 100644 --- a/packages/contracts/test/exchange/wrapper.ts +++ b/packages/contracts/test/exchange/wrapper.ts @@ -1,385 +1,1259 @@ -import { SignedOrder, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils'; +import { RevertReason, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as chai from 'chai'; import * as _ from 'lodash'; -import * as Web3 from 'web3'; - -import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token'; -import { - ExchangeContract, - LogCancelContractEventArgs, - LogErrorContractEventArgs, - LogFillContractEventArgs, -} from '../../src/contract_wrappers/generated/exchange'; -import { TokenRegistryContract } from '../../src/contract_wrappers/generated/token_registry'; -import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; -import { Balances } from '../../util/balances'; -import { constants } from '../../util/constants'; -import { ExchangeWrapper } from '../../util/exchange_wrapper'; -import { OrderFactory } from '../../util/order_factory'; -import { BalancesByOwner, ContractName } from '../../util/types'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { getLatestBlockTimestampAsync, increaseTimeAndMineBlockAsync } from '../utils/block_timestamp'; import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { ERC20BalancesByOwner, OrderStatus } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -describe('Exchange', () => { - let maker: string; - let tokenOwner: string; - let taker: string; - let feeRecipient: string; - - const INIT_BAL = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); - const INIT_ALLOW = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); +describe('Exchange wrappers', () => { + let makerAddress: string; + let owner: string; + let takerAddress: string; + let feeRecipientAddress: string; - let rep: DummyTokenContract; - let dgd: DummyTokenContract; - let zrx: DummyTokenContract; + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; let exchange: ExchangeContract; - let tokenRegistry: TokenRegistryContract; - let tokenTransferProxy: TokenTransferProxyContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; - let balances: BalancesByOwner; - - let exWrapper: ExchangeWrapper; - let dmyBalances: Balances; + 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(); - tokenOwner = accounts[0]; - [maker, taker, feeRecipient] = accounts; - const [repInstance, dgdInstance, zrxInstance] = await Promise.all([ - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), - ]); - rep = new DummyTokenContract(web3Wrapper, repInstance.abi, repInstance.address); - dgd = new DummyTokenContract(web3Wrapper, dgdInstance.abi, dgdInstance.address); - zrx = new DummyTokenContract(web3Wrapper, zrxInstance.abi, zrxInstance.address); - const tokenRegistryInstance = await deployer.deployAsync(ContractName.TokenRegistry); - tokenRegistry = new TokenRegistryContract( - web3Wrapper, - tokenRegistryInstance.abi, - tokenRegistryInstance.address, + 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, ); - const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); - tokenTransferProxy = new TokenTransferProxyContract( - web3Wrapper, - tokenTransferProxyInstance.abi, - tokenTransferProxyInstance.address, + 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), ); - const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ - zrx.address, - tokenTransferProxy.address, - ]); - exchange = new ExchangeContract(web3Wrapper, exchangeInstance.abi, exchangeInstance.address); - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] }); - const zeroEx = new ZeroEx(web3.currentProvider, { networkId: constants.TESTRPC_NETWORK_ID }); - exWrapper = new ExchangeWrapper(exchange, zeroEx); + 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, + ); + + defaultMakerAssetAddress = erc20TokenA.address; + defaultTakerAssetAddress = erc20TokenB.address; const defaultOrderParams = { - exchangeContractAddress: exchange.address, - maker, - feeRecipient, - makerTokenAddress: rep.address, - takerTokenAddress: dgd.address, - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), - makerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), - takerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), + ...constants.STATIC_ORDER_PARAMS, + exchangeAddress: exchange.address, + makerAddress, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), }; - - orderFactory = new OrderFactory(zeroEx, defaultOrderParams); - dmyBalances = new Balances([rep, dgd, zrx], [maker, taker, feeRecipient]); - await Promise.all([ - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: maker }), - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: taker }), - rep.setBalance.sendTransactionAsync(maker, INIT_BAL, { from: tokenOwner }), - rep.setBalance.sendTransactionAsync(taker, INIT_BAL, { from: tokenOwner }), - dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: maker }), - dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: taker }), - dgd.setBalance.sendTransactionAsync(maker, INIT_BAL, { from: tokenOwner }), - dgd.setBalance.sendTransactionAsync(taker, INIT_BAL, { from: tokenOwner }), - zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: maker }), - zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { from: taker }), - zrx.setBalance.sendTransactionAsync(maker, INIT_BAL, { from: tokenOwner }), - zrx.setBalance.sendTransactionAsync(taker, INIT_BAL, { from: tokenOwner }), - ]); + 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', () => { - beforeEach(async () => { - balances = await dmyBalances.getAsync(); + 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 transfer the correct amounts', async () => { + it('should throw if a signedOrder is expired', async () => { + const currentTimestamp = await getLatestBlockTimestampAsync(); const signedOrder = await orderFactory.newSignedOrderAsync({ - makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), - takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), + expirationTimeSeconds: new BigNumber(currentTimestamp).sub(10), }); - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - await exWrapper.fillOrKillOrderAsync(signedOrder, taker, { - fillTakerTokenAmount, + + 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), }); - const newBalances = await dmyBalances.getAsync(); + return expectTransactionFailedAsync( + exchangeWrapper.fillOrKillOrderAsync(signedOrder, takerAddress), + RevertReason.CompleteFillFailed, + ); + }); + }); - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); + describe('fillOrderNoThrow', () => { + 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(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); const takerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - expect(newBalances[maker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.makerTokenAddress].minus(fillMakerTokenAmount), + .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[maker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrder.takerTokenAddress].add(fillTakerTokenAmount), + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFee), ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal(balances[maker][zrx.address].minus(makerFee)); - expect(newBalances[taker][signedOrder.takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.takerTokenAddress].minus(fillTakerTokenAmount), + expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), ); - expect(newBalances[taker][signedOrder.makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrder.makerTokenAddress].add(fillMakerTokenAmount), + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount), ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal(balances[taker][zrx.address].minus(takerFee)); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(makerFee.add(takerFee)), + 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 an signedOrder is expired', async () => { + it('should not change erc20Balances if maker erc20Balances are too low to fill order', async () => { const signedOrder = await orderFactory.newSignedOrderAsync({ - expirationUnixTimestampSec: new BigNumber(Math.floor((Date.now() - 10000) / 1000)), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100000), 18), }); - return expect(exWrapper.fillOrKillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); + await exchangeWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); }); - it('should throw if entire fillTakerTokenAmount not filled', async () => { + 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 from = taker; - await exWrapper.fillOrderAsync(signedOrder, from, { - fillTakerTokenAmount: signedOrder.takerTokenAmount.div(2), + 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); + }); - return expect(exWrapper.fillOrKillOrderAsync(signedOrder, taker)).to.be.rejectedWith(constants.REVERT); + 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 Promise.all([ - orderFactory.newSignedOrderAsync(), - orderFactory.newSignedOrderAsync(), - orderFactory.newSignedOrderAsync(), - ]); - balances = await dmyBalances.getAsync(); + signedOrders = [ + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + ]; }); describe('batchFillOrders', () => { it('should transfer the correct amounts', async () => { - const fillTakerTokenAmounts: BigNumber[] = []; - const makerTokenAddress = rep.address; - const takerTokenAddress = dgd.address; - signedOrders.forEach(signedOrder => { - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); + 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(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); const takerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - fillTakerTokenAmounts.push(fillTakerTokenAmount); - balances[maker][makerTokenAddress] = balances[maker][makerTokenAddress].minus(fillMakerTokenAmount); - balances[maker][takerTokenAddress] = balances[maker][takerTokenAddress].add(fillTakerTokenAmount); - balances[maker][zrx.address] = balances[maker][zrx.address].minus(makerFee); - balances[taker][makerTokenAddress] = balances[taker][makerTokenAddress].add(fillMakerTokenAmount); - balances[taker][takerTokenAddress] = balances[taker][takerTokenAddress].minus(fillTakerTokenAmount); - balances[taker][zrx.address] = balances[taker][zrx.address].minus(takerFee); - balances[feeRecipient][zrx.address] = balances[feeRecipient][zrx.address].add( - makerFee.add(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 exWrapper.batchFillOrdersAsync(signedOrders, taker, { - fillTakerTokenAmounts, + await exchangeWrapper.batchFillOrdersAsync(signedOrders, takerAddress, { + takerAssetFillAmounts, }); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); }); }); describe('batchFillOrKillOrders', () => { it('should transfer the correct amounts', async () => { - const fillTakerTokenAmounts: BigNumber[] = []; - const makerTokenAddress = rep.address; - const takerTokenAddress = dgd.address; - signedOrders.forEach(signedOrder => { - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - const fillMakerTokenAmount = fillTakerTokenAmount - .times(signedOrder.makerTokenAmount) - .dividedToIntegerBy(signedOrder.takerTokenAmount); + 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(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); + .times(makerAssetFilledAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); const takerFee = signedOrder.takerFee - .times(fillMakerTokenAmount) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - fillTakerTokenAmounts.push(fillTakerTokenAmount); - balances[maker][makerTokenAddress] = balances[maker][makerTokenAddress].minus(fillMakerTokenAmount); - balances[maker][takerTokenAddress] = balances[maker][takerTokenAddress].add(fillTakerTokenAmount); - balances[maker][zrx.address] = balances[maker][zrx.address].minus(makerFee); - balances[taker][makerTokenAddress] = balances[taker][makerTokenAddress].add(fillMakerTokenAmount); - balances[taker][takerTokenAddress] = balances[taker][takerTokenAddress].minus(fillTakerTokenAmount); - balances[taker][zrx.address] = balances[taker][zrx.address].minus(takerFee); - balances[feeRecipient][zrx.address] = balances[feeRecipient][zrx.address].add( - makerFee.add(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 exWrapper.batchFillOrKillOrdersAsync(signedOrders, taker, { - fillTakerTokenAmounts, + await exchangeWrapper.batchFillOrKillOrdersAsync(signedOrders, takerAddress, { + takerAssetFillAmounts, }); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); + 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 fillTakerTokenAmounts: BigNumber[] = []; - signedOrders.forEach(signedOrder => { - const fillTakerTokenAmount = signedOrder.takerTokenAmount.div(2); - fillTakerTokenAmounts.push(fillTakerTokenAmount); + 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 () => { + 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, }); - await exWrapper.fillOrKillOrderAsync(signedOrders[0], taker); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); + }); + }); + + describe('marketSellOrders', () => { + 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(); - return expect( - exWrapper.batchFillOrKillOrdersAsync(signedOrders, taker, { - fillTakerTokenAmounts, + 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), }), - ).to.be.rejectedWith(constants.REVERT); + // 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('fillOrdersUpTo', () => { - it('should stop when the entire fillTakerTokenAmount is filled', async () => { - const fillTakerTokenAmount = signedOrders[0].takerTokenAmount.plus( - signedOrders[1].takerTokenAmount.div(2), + describe('marketSellOrdersNoThrow', () => { + it('should stop when the entire takerAssetFillAmount is filled', async () => { + const takerAssetFillAmount = signedOrders[0].takerAssetAmount.plus( + signedOrders[1].takerAssetAmount.div(2), ); - await exWrapper.fillOrdersUpToAsync(signedOrders, taker, { - fillTakerTokenAmount, + 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 dmyBalances.getAsync(); + const newBalances = await erc20Wrapper.getBalancesAsync(); - const fillMakerTokenAmount = signedOrders[0].makerTokenAmount.add( - signedOrders[1].makerTokenAmount.dividedToIntegerBy(2), + 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[maker][signedOrders[0].makerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrders[0].makerTokenAddress].minus(fillMakerTokenAmount), + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFilledAmount), ); - expect(newBalances[maker][signedOrders[0].takerTokenAddress]).to.be.bignumber.equal( - balances[maker][signedOrders[0].takerTokenAddress].add(fillTakerTokenAmount), + expect(newBalances[makerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), ); - expect(newBalances[maker][zrx.address]).to.be.bignumber.equal( - balances[maker][zrx.address].minus(makerFee), + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFee), ); - expect(newBalances[taker][signedOrders[0].takerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrders[0].takerTokenAddress].minus(fillTakerTokenAmount), + expect(newBalances[takerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), ); - expect(newBalances[taker][signedOrders[0].makerTokenAddress]).to.be.bignumber.equal( - balances[taker][signedOrders[0].makerTokenAddress].add(fillMakerTokenAmount), + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].add(makerAssetFilledAmount), ); - expect(newBalances[taker][zrx.address]).to.be.bignumber.equal( - balances[taker][zrx.address].minus(takerFee), + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFee), ); - expect(newBalances[feeRecipient][zrx.address]).to.be.bignumber.equal( - balances[feeRecipient][zrx.address].add(makerFee.add(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 fillTakerTokenAmount', async () => { - const fillTakerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(100000), 18); - signedOrders.forEach(signedOrder => { - balances[maker][signedOrder.makerTokenAddress] = balances[maker][ - signedOrder.makerTokenAddress - ].minus(signedOrder.makerTokenAmount); - balances[maker][signedOrder.takerTokenAddress] = balances[maker][signedOrder.takerTokenAddress].add( - signedOrder.takerTokenAmount, + 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, ); - balances[maker][zrx.address] = balances[maker][zrx.address].minus(signedOrder.makerFee); - balances[taker][signedOrder.makerTokenAddress] = balances[taker][signedOrder.makerTokenAddress].add( - signedOrder.makerTokenAmount, + 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, ); - balances[taker][signedOrder.takerTokenAddress] = balances[taker][ - signedOrder.takerTokenAddress - ].minus(signedOrder.takerTokenAmount); - balances[taker][zrx.address] = balances[taker][zrx.address].minus(signedOrder.takerFee); - balances[feeRecipient][zrx.address] = balances[feeRecipient][zrx.address].add( - signedOrder.makerFee.add(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', () => { + 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 exWrapper.fillOrdersUpToAsync(signedOrders, taker, { - fillTakerTokenAmount, + await exchangeWrapper.marketBuyOrdersAsync(signedOrders, takerAddress, { + makerAssetFillAmount, }); - const newBalances = await dmyBalances.getAsync(); - expect(newBalances).to.be.deep.equal(balances); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.be.deep.equal(erc20Balances); }); - it('should throw when an signedOrder does not use the same takerTokenAddress', async () => { - signedOrders = await Promise.all([ - orderFactory.newSignedOrderAsync(), - orderFactory.newSignedOrderAsync({ takerTokenAddress: zrx.address }), - orderFactory.newSignedOrderAsync(), - ]); + 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 expect( - exWrapper.fillOrdersUpToAsync(signedOrders, taker, { - fillTakerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(1000), 18), + return expectTransactionFailedAsync( + exchangeWrapper.marketBuyOrdersAsync(signedOrders, takerAddress, { + makerAssetFillAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), 18), }), - ).to.be.rejectedWith(constants.REVERT); + RevertReason.InvalidOrderSignature, + ); + }); + }); + + describe('marketBuyOrdersNoThrow', () => { + 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 cancelTakerTokenAmounts = _.map(signedOrders, signedOrder => signedOrder.takerTokenAmount); - await exWrapper.batchCancelOrdersAsync(signedOrders, maker, { - cancelTakerTokenAmounts, + 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); + }); + }); - await exWrapper.batchFillOrdersAsync(signedOrders, taker, { - fillTakerTokenAmounts: cancelTakerTokenAmounts, + 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); }); - const newBalances = await dmyBalances.getAsync(); - expect(balances).to.be.deep.equal(newBalances); + }); + 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/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts new file mode 100644 index 000000000..28ffdeabe --- /dev/null +++ b/packages/contracts/test/forwarder/forwarder.ts @@ -0,0 +1,996 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { assetDataUtils } from '@0xproject/order-utils'; +import { RevertReason, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; +import { ForwarderContract } from '../../generated_contract_wrappers/forwarder'; +import { WETH9Contract } from '../../generated_contract_wrappers/weth9'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { ForwarderWrapper } from '../utils/forwarder_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { ContractName, ERC20BalancesByOwner } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const DECIMALS_DEFAULT = 18; +const MAX_WETH_FILL_PERCENTAGE = 95; + +describe(ContractName.Forwarder, () => { + let makerAddress: string; + let owner: string; + let takerAddress: string; + let feeRecipientAddress: string; + let otherAddress: string; + let defaultMakerAssetAddress: string; + let zrxAssetData: string; + + let weth: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; + let forwarderContract: ForwarderContract; + let wethContract: WETH9Contract; + let forwarderWrapper: ForwarderWrapper; + let exchangeWrapper: ExchangeWrapper; + + let orderWithoutFee: SignedOrder; + let orderWithFee: SignedOrder; + let feeOrder: SignedOrder; + let orderFactory: OrderFactory; + let erc20Wrapper: ERC20Wrapper; + let erc20Balances: ERC20BalancesByOwner; + let tx: TransactionReceiptWithDecodedLogs; + + let erc721MakerAssetIds: BigNumber[]; + let takerEthBalanceBefore: BigNumber; + let feePercentage: BigNumber; + let gasPrice: BigNumber; + + before(async () => { + await blockchainLifecycle.startAsync(); + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress, otherAddress] = accounts); + + const txHash = await web3Wrapper.sendTransactionAsync({ from: accounts[0], to: accounts[0], value: 0 }); + const transaction = await web3Wrapper.getTransactionByHashAsync(txHash); + gasPrice = new BigNumber(transaction.gasPrice); + + const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + + const numDummyErc20ToDeploy = 3; + let erc20TokenA; + [erc20TokenA, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + const erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + const erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address]; + + wethContract = await WETH9Contract.deployFrom0xArtifactAsync(artifacts.EtherToken, provider, txDefaults); + weth = new DummyERC20TokenContract(wethContract.abi, wethContract.address, provider); + erc20Wrapper.addDummyTokenContract(weth); + + const wethAssetData = assetDataUtils.encodeERC20AssetData(wethContract.address); + zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + const exchangeContract = new ExchangeContract(exchangeInstance.abi, exchangeInstance.address, provider); + exchangeWrapper = new ExchangeWrapper(exchangeContract, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); + + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { + from: owner, + }); + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { + from: owner, + }); + + defaultMakerAssetAddress = erc20TokenA.address; + const defaultTakerAssetAddress = wethContract.address; + const defaultOrderParams = { + exchangeAddress: exchangeInstance.address, + makerAddress, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT), + }; + const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + orderFactory = new OrderFactory(privateKey, defaultOrderParams); + + const forwarderInstance = await ForwarderContract.deployFrom0xArtifactAsync( + artifacts.Forwarder, + provider, + txDefaults, + exchangeInstance.address, + wethContract.address, + zrxToken.address, + zrxAssetData, + wethAssetData, + ); + forwarderContract = new ForwarderContract(forwarderInstance.abi, forwarderInstance.address, provider); + forwarderWrapper = new ForwarderWrapper(forwarderContract, provider); + const zrxDepositAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transfer.sendTransactionAsync(forwarderContract.address, zrxDepositAmount), + ); + erc20Wrapper.addTokenOwnerAddress(forwarderInstance.address); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + erc20Balances = await erc20Wrapper.getBalancesAsync(); + takerEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + orderWithoutFee = await orderFactory.newSignedOrderAsync(); + feeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + orderWithFee = await orderFactory.newSignedOrderAsync({ + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('marketSellOrdersWithEth without extra fees', () => { + it('should fill a single order', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill multiple orders', async () => { + const secondOrderWithoutFee = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, secondOrderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = ordersWithoutFee[0].takerAssetAmount.plus( + ordersWithoutFee[1].takerAssetAmount.dividedToIntegerBy(2), + ); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const firstTakerAssetFillAmount = ordersWithoutFee[0].takerAssetAmount; + const secondTakerAssetFillAmount = primaryTakerAssetFillAmount.minus(firstTakerAssetFillAmount); + + const makerAssetFillAmount = ordersWithoutFee[0].makerAssetAmount.plus( + ordersWithoutFee[1].makerAssetAmount + .times(secondTakerAssetFillAmount) + .dividedToIntegerBy(ordersWithoutFee[1].takerAssetAmount), + ); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill the order and pay ZRX fees from a single feeOrder', async () => { + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const feeAmount = ForwarderWrapper.getPercentageOfValue( + orderWithFee.takerFee.dividedToIntegerBy(2), + MAX_WETH_FILL_PERCENTAGE, + ); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill the orders and pay ZRX from multiple feeOrders', async () => { + const ordersWithFee = [orderWithFee]; + const ethValue = orderWithFee.takerAssetAmount; + const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const makerAssetAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const takerAssetAmount = feeOrder.takerAssetAmount + .times(makerAssetAmount) + .dividedToIntegerBy(feeOrder.makerAssetAmount); + + const firstFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const secondFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const feeOrders = [firstFeeOrder, secondFeeOrder]; + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const feeAmount = ForwarderWrapper.getPercentageOfValue(orderWithFee.takerFee, MAX_WETH_FILL_PERCENTAGE); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fill the order when token is ZRX with fees', async () => { + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + const takerFeePaid = orderWithFee.takerFee.dividedToIntegerBy(2); + const makerFeePaid = orderWithFee.makerFee.dividedToIntegerBy(2); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount).minus(makerFeePaid), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount).minus(takerFeePaid), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(ethValue), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[forwarderContract.address][zrxToken.address], + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should refund remaining ETH if amount is greater than takerAssetAmount', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.times(2); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const totalEthSpent = orderWithoutFee.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + }); + it('should revert if ZRX cannot be fully repurchased', async () => { + orderWithFee = await orderFactory.newSignedOrderAsync({ + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + feeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const feeOrders = [feeOrder]; + const ethValue = orderWithFee.takerAssetAmount; + return expectTransactionFailedAsync( + forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithFee, feeOrders, { + value: ethValue, + from: takerAddress, + }), + RevertReason.CompleteFillFailed, + ); + }); + it('should not fill orders with different makerAssetData than the first order', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + const erc721SignedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + }); + const erc20SignedOrder = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [erc20SignedOrder, erc721SignedOrder]; + const feeOrders: SignedOrder[] = []; + const ethValue = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount); + + tx = await forwarderWrapper.marketSellOrdersWithEthAsync(ordersWithoutFee, feeOrders, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const totalEthSpent = erc20SignedOrder.takerAssetAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + }); + }); + describe('marketSellOrdersWithEth with extra fees', () => { + it('should fill the order and send fee to feeRecipient', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + tx = await forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getPercentageOfValue( + ethValue, + MAX_WETH_FILL_PERCENTAGE, + ); + const makerAssetFillAmount = primaryTakerAssetFillAmount + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); + const totalEthSpent = primaryTakerAssetFillAmount.plus(ethSpentOnFee).plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore.plus(ethSpentOnFee)); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fail if the fee is set too high', async () => { + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + const baseFeePercentage = 6; + feePercentage = ForwarderWrapper.getPercentageOfValue(ethValue, baseFeePercentage); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + await expectTransactionFailedAsync( + forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + { from: takerAddress, value: ethValue, gasPrice }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.FeePercentageTooLarge, + ); + }); + it('should fail if there is not enough ETH remaining to pay the fee', async () => { + const ethValue = orderWithoutFee.takerAssetAmount.div(2); + const baseFeePercentage = 5; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + await expectTransactionFailedAsync( + forwarderWrapper.marketSellOrdersWithEthAsync( + ordersWithFee, + feeOrders, + { from: takerAddress, value: ethValue, gasPrice }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.InsufficientEthRemaining, + ); + }); + }); + describe('marketBuyOrdersWithEth without extra fees', () => { + it('should buy the exact amount of makerAsset in a single order', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy the exact amount of makerAsset in multiple orders', async () => { + const secondOrderWithoutFee = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, secondOrderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = ordersWithoutFee[0].makerAssetAmount.plus( + ordersWithoutFee[1].makerAssetAmount.dividedToIntegerBy(2), + ); + const ethValue = ordersWithoutFee[0].takerAssetAmount.plus( + ordersWithoutFee[1].takerAssetAmount.dividedToIntegerBy(2), + ); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy the exact amount of makerAsset and return excess ETH', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue.dividedToIntegerBy(2); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy the exact amount of makerAsset and pay ZRX from feeOrders', async () => { + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount.dividedToIntegerBy(2); + const feeAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const totalEthSpent = primaryTakerAssetFillAmount + .plus(wethSpentOnFeeOrders) + .plus(gasPrice.times(tx.gasUsed)); + + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy slightly greater than makerAssetAmount when buying ZRX', async () => { + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(zrxToken.address), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithFee.takerAssetAmount; + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ForwarderWrapper.getWethForFeeOrders( + makerAssetFillAmount, + ordersWithFee, + ); + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + const makerAssetFilledAmount = orderWithFee.makerAssetAmount + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const takerFeePaid = orderWithFee.takerFee + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const makerFeePaid = orderWithFee.makerFee + .times(primaryTakerAssetFillAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + const totalZrxPurchased = makerAssetFilledAmount.minus(takerFeePaid); + // Up to 1 wei worth of ZRX will be overbought per order + const maxOverboughtZrx = new BigNumber(1) + .times(orderWithFee.makerAssetAmount) + .dividedToIntegerBy(orderWithFee.takerAssetAmount); + + expect(totalZrxPurchased).to.be.bignumber.gte(makerAssetFillAmount); + expect(totalZrxPurchased).to.be.bignumber.lte(makerAssetFillAmount.plus(maxOverboughtZrx)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFilledAmount).minus(makerFeePaid), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(totalZrxPurchased), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[forwarderContract.address][zrxToken.address], + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(4); + return expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }), + RevertReason.CompleteFillFailed, + ); + }); + it('should buy an ERC721 asset from a single order', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = new BigNumber(1); + const ethValue = orderWithFee.takerAssetAmount; + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + from: takerAddress, + value: ethValue, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should revert if buying an ERC721 asset when later orders contain different makerAssetData', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + }); + const differentMakerAssetDataOrder = await orderFactory.newSignedOrderAsync(); + const ordersWithoutFee = [orderWithoutFee, differentMakerAssetDataOrder]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = new BigNumber(1).plus(differentMakerAssetDataOrder.makerAssetAmount); + const ethValue = orderWithFee.takerAssetAmount; + return expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }), + RevertReason.CompleteFillFailed, + ); + }); + it('should buy an ERC721 asset and pay ZRX fees from a single fee order', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const feeOrders = [feeOrder]; + const makerAssetFillAmount = orderWithFee.makerAssetAmount; + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; + const feeAmount = orderWithFee.takerFee; + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should buy an ERC721 asset and pay ZRX fees from multiple fee orders', async () => { + const makerAssetId = erc721MakerAssetIds[0]; + orderWithFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT), + }); + const ordersWithFee = [orderWithFee]; + const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const makerAssetAmount = orderWithFee.takerFee.dividedToIntegerBy(2); + const takerAssetAmount = feeOrder.takerAssetAmount + .times(makerAssetAmount) + .dividedToIntegerBy(feeOrder.makerAssetAmount); + + const firstFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const secondFeeOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + makerAssetAmount, + takerAssetAmount, + }); + const feeOrders = [firstFeeOrder, secondFeeOrder]; + + const makerAssetFillAmount = orderWithFee.makerAssetAmount; + const primaryTakerAssetFillAmount = orderWithFee.takerAssetAmount; + const feeAmount = orderWithFee.takerFee; + const wethSpentOnFeeOrders = ForwarderWrapper.getWethForFeeOrders(feeAmount, feeOrders); + const ethValue = primaryTakerAssetFillAmount.plus(wethSpentOnFeeOrders); + + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const totalEthSpent = ethValue.plus(gasPrice.times(tx.gasUsed)); + + expect(newOwner).to.be.bignumber.equal(takerAddress); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount).plus(wethSpentOnFeeOrders), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('marketBuyOrdersWithEth with extra fees', () => { + it('should buy an asset and send fee to feeRecipient', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ); + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + + const primaryTakerAssetFillAmount = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + const ethSpentOnFee = ForwarderWrapper.getPercentageOfValue(primaryTakerAssetFillAmount, baseFeePercentage); + const totalEthSpent = primaryTakerAssetFillAmount.plus(ethSpentOnFee).plus(gasPrice.times(tx.gasUsed)); + + expect(feeRecipientEthBalanceAfter).to.be.bignumber.equal(feeRecipientEthBalanceBefore.plus(ethSpentOnFee)); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('should fail if the fee is set too high', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount; + + const baseFeePercentage = 6; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + await expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.FeePercentageTooLarge, + ); + }); + it('should fail if there is not enough ETH remaining to pay the fee', async () => { + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = orderWithoutFee.makerAssetAmount.dividedToIntegerBy(2); + const ethValue = orderWithoutFee.takerAssetAmount.dividedToIntegerBy(2); + + const baseFeePercentage = 2; + feePercentage = ForwarderWrapper.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, baseFeePercentage); + await expectTransactionFailedAsync( + forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + makerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + { feePercentage, feeRecipient: feeRecipientAddress }, + ), + RevertReason.InsufficientEthRemaining, + ); + }); + }); + describe('withdrawAsset', () => { + it('should allow owner to withdraw ERC20 tokens', async () => { + const zrxWithdrawAmount = erc20Balances[forwarderContract.address][zrxToken.address]; + await forwarderWrapper.withdrawAssetAsync(zrxAssetData, zrxWithdrawAmount, { from: owner }); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[owner][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[owner][zrxToken.address].plus(zrxWithdrawAmount), + ); + expect(newBalances[forwarderContract.address][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[forwarderContract.address][zrxToken.address].minus(zrxWithdrawAmount), + ); + }); + it('should revert if not called by owner', async () => { + const zrxWithdrawAmount = erc20Balances[forwarderContract.address][zrxToken.address]; + await expectTransactionFailedAsync( + forwarderWrapper.withdrawAssetAsync(zrxAssetData, zrxWithdrawAmount, { from: makerAddress }), + RevertReason.OnlyContractOwner, + ); + }); + }); +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/global_hooks.ts b/packages/contracts/test/global_hooks.ts index 089521d94..cf7c52efd 100644 --- a/packages/contracts/test/global_hooks.ts +++ b/packages/contracts/test/global_hooks.ts @@ -1,8 +1,15 @@ -import { coverage, env, EnvVars } from '@0xproject/dev-utils'; +import { env, EnvVars } from '@0xproject/dev-utils'; + +import { coverage } from './utils/coverage'; +import { profiler } from './utils/profiler'; 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(); + } }); diff --git a/packages/contracts/test/libraries/lib_bytes.ts b/packages/contracts/test/libraries/lib_bytes.ts new file mode 100644 index 000000000..1c497a226 --- /dev/null +++ b/packages/contracts/test/libraries/lib_bytes.ts @@ -0,0 +1,834 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import BN = require('bn.js'); +import * as chai from 'chai'; +import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; + +import { TestLibBytesContract } from '../../generated_contract_wrappers/test_lib_bytes'; +import { artifacts } from '../utils/artifacts'; +import { expectContractCallFailed } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { typeEncodingUtils } from '../utils/type_encoding_utils'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +// BUG: Ideally we would use Buffer.from(memory).toString('hex') +// https://github.com/Microsoft/TypeScript/issues/23155 +const toHex = (buf: Uint8Array): string => buf.reduce((a, v) => a + ('00' + v.toString(16)).slice(-2), '0x'); + +const fromHex = (str: string): Uint8Array => Uint8Array.from(Buffer.from(str.slice(2), 'hex')); + +describe('LibBytes', () => { + let libBytes: TestLibBytesContract; + const byteArrayShorterThan32Bytes = '0x012345'; + const byteArrayShorterThan20Bytes = byteArrayShorterThan32Bytes; + const byteArrayLongerThan32Bytes = + '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const byteArrayLongerThan32BytesFirstBytesSwapped = + '0x2301456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const byteArrayLongerThan32BytesLastBytesSwapped = + '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abefcd'; + let testAddress: string; + let testAddressB: string; + const testBytes32 = '0x102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f01020'; + const testBytes32B = '0x534877abd8443578526845cdfef020047528759477fedef87346527659aced32'; + const testUint256 = new BigNumber(testBytes32, 16); + const testUint256B = new BigNumber(testBytes32B, 16); + let shortData: string; + let shortTestBytes: string; + let shortTestBytesAsBuffer: Buffer; + let wordOfData: string; + let wordOfTestBytes: string; + let wordOfTestBytesAsBuffer: Buffer; + let longData: string; + let longTestBytes: string; + let longTestBytesAsBuffer: Buffer; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + // Setup accounts & addresses + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + testAddress = accounts[1]; + testAddressB = accounts[2]; + // Deploy LibBytes + libBytes = await TestLibBytesContract.deployFrom0xArtifactAsync(artifacts.TestLibBytes, provider, txDefaults); + // Verify lengths of test data + const byteArrayShorterThan32BytesLength = ethUtil.toBuffer(byteArrayShorterThan32Bytes).byteLength; + expect(byteArrayShorterThan32BytesLength).to.be.lessThan(32); + const byteArrayLongerThan32BytesLength = ethUtil.toBuffer(byteArrayLongerThan32Bytes).byteLength; + expect(byteArrayLongerThan32BytesLength).to.be.greaterThan(32); + const testBytes32Length = ethUtil.toBuffer(testBytes32).byteLength; + expect(testBytes32Length).to.be.equal(32); + // Create short test bytes + shortData = '0xffffaa'; + const encodedShortData = ethUtil.toBuffer(shortData); + const shortDataLength = new BigNumber(encodedShortData.byteLength); + const encodedShortDataLength = typeEncodingUtils.encodeUint256(shortDataLength); + shortTestBytesAsBuffer = Buffer.concat([encodedShortDataLength, encodedShortData]); + shortTestBytes = ethUtil.bufferToHex(shortTestBytesAsBuffer); + // Create test bytes one word in length + wordOfData = ethUtil.bufferToHex(typeEncodingUtils.encodeUint256(generatePseudoRandomSalt())); + const encodedWordOfData = ethUtil.toBuffer(wordOfData); + const wordOfDataLength = new BigNumber(encodedWordOfData.byteLength); + const encodedWordOfDataLength = typeEncodingUtils.encodeUint256(wordOfDataLength); + wordOfTestBytesAsBuffer = Buffer.concat([encodedWordOfDataLength, encodedWordOfData]); + wordOfTestBytes = ethUtil.bufferToHex(wordOfTestBytesAsBuffer); + // Create long test bytes (combines short test bytes with word of test bytes) + longData = ethUtil.bufferToHex(Buffer.concat([encodedShortData, encodedWordOfData])); + const longDataLength = new BigNumber(encodedShortData.byteLength + encodedWordOfData.byteLength); + const encodedLongDataLength = typeEncodingUtils.encodeUint256(longDataLength); + longTestBytesAsBuffer = Buffer.concat([encodedLongDataLength, encodedShortData, encodedWordOfData]); + longTestBytes = ethUtil.bufferToHex(longTestBytesAsBuffer); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('popLastByte', () => { + it('should revert if length is 0', async () => { + return expectContractCallFailed( + libBytes.publicPopLastByte.callAsync(constants.NULL_BYTES), + RevertReason.LibBytesGreaterThanZeroLengthRequired, + ); + }); + it('should pop the last byte from the input and return it', async () => { + const [newBytes, poppedByte] = await libBytes.publicPopLastByte.callAsync(byteArrayLongerThan32Bytes); + const expectedNewBytes = byteArrayLongerThan32Bytes.slice(0, -2); + const expectedPoppedByte = `0x${byteArrayLongerThan32Bytes.slice(-2)}`; + expect(newBytes).to.equal(expectedNewBytes); + expect(poppedByte).to.equal(expectedPoppedByte); + }); + }); + + describe('popLast20Bytes', () => { + it('should revert if length is less than 20', async () => { + return expectContractCallFailed( + libBytes.publicPopLast20Bytes.callAsync(byteArrayShorterThan20Bytes), + RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + ); + }); + it('should pop the last 20 bytes from the input and return it', async () => { + const [newBytes, poppedAddress] = await libBytes.publicPopLast20Bytes.callAsync(byteArrayLongerThan32Bytes); + const expectedNewBytes = byteArrayLongerThan32Bytes.slice(0, -40); + const expectedPoppedAddress = `0x${byteArrayLongerThan32Bytes.slice(-40)}`; + expect(newBytes).to.equal(expectedNewBytes); + expect(poppedAddress).to.equal(expectedPoppedAddress); + }); + }); + + describe('equals', () => { + it('should return true if byte arrays are equal (both arrays < 32 bytes)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayShorterThan32Bytes, + byteArrayShorterThan32Bytes, + ); + return expect(isEqual).to.be.true(); + }); + it('should return true if byte arrays are equal (both arrays > 32 bytes)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayLongerThan32Bytes, + byteArrayLongerThan32Bytes, + ); + return expect(isEqual).to.be.true(); + }); + it('should return false if byte arrays are not equal (first array < 32 bytes, second array > 32 bytes)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayShorterThan32Bytes, + byteArrayLongerThan32Bytes, + ); + return expect(isEqual).to.be.false(); + }); + it('should return false if byte arrays are not equal (first array > 32 bytes, second array < 32 bytes)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayLongerThan32Bytes, + byteArrayShorterThan32Bytes, + ); + return expect(isEqual).to.be.false(); + }); + it('should return false if byte arrays are not equal (same length, but a byte in first word differs)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayLongerThan32BytesFirstBytesSwapped, + byteArrayLongerThan32Bytes, + ); + return expect(isEqual).to.be.false(); + }); + it('should return false if byte arrays are not equal (same length, but a byte in last word differs)', async () => { + const isEqual = await libBytes.publicEquals.callAsync( + byteArrayLongerThan32BytesLastBytesSwapped, + byteArrayLongerThan32Bytes, + ); + return expect(isEqual).to.be.false(); + }); + + describe('should ignore trailing data', () => { + it('should return true when both < 32 bytes', async () => { + const isEqual = await libBytes.publicEqualsPop1.callAsync('0x0102', '0x0103'); + return expect(isEqual).to.be.true(); + }); + }); + }); + + describe('deepCopyBytes', () => { + it('should revert if dest is shorter than source', async () => { + return expectContractCallFailed( + libBytes.publicDeepCopyBytes.callAsync(byteArrayShorterThan32Bytes, byteArrayLongerThan32Bytes), + RevertReason.LibBytesGreaterOrEqualToSourceBytesLengthRequired, + ); + }); + it('should overwrite dest with source if source and dest have equal length', async () => { + const zeroedByteArrayLongerThan32Bytes = `0x${_.repeat('0', byteArrayLongerThan32Bytes.length - 2)}`; + const zeroedBytesAfterCopy = await libBytes.publicDeepCopyBytes.callAsync( + zeroedByteArrayLongerThan32Bytes, + byteArrayLongerThan32Bytes, + ); + return expect(zeroedBytesAfterCopy).to.be.equal(byteArrayLongerThan32Bytes); + }); + it('should overwrite the leftmost len(source) bytes of dest if dest is larger than source', async () => { + const zeroedByteArrayLongerThan32Bytes = `0x${_.repeat('0', byteArrayLongerThan32Bytes.length * 2)}`; + const zeroedBytesAfterCopy = await libBytes.publicDeepCopyBytes.callAsync( + zeroedByteArrayLongerThan32Bytes, + byteArrayLongerThan32Bytes, + ); + const copiedBytes = zeroedBytesAfterCopy.slice(0, byteArrayLongerThan32Bytes.length); + return expect(copiedBytes).to.be.equal(byteArrayLongerThan32Bytes); + }); + it('should not overwrite the rightmost bytes of dest if dest is larger than source', async () => { + const zeroedByteArrayLongerThan32Bytes = `0x${_.repeat('0', byteArrayLongerThan32Bytes.length * 2)}`; + const zeroedBytesAfterCopy = await libBytes.publicDeepCopyBytes.callAsync( + zeroedByteArrayLongerThan32Bytes, + byteArrayLongerThan32Bytes, + ); + const expectedNotCopiedBytes = zeroedByteArrayLongerThan32Bytes.slice(byteArrayLongerThan32Bytes.length); + const notCopiedBytes = zeroedBytesAfterCopy.slice(byteArrayLongerThan32Bytes.length); + return expect(notCopiedBytes).to.be.equal(expectedNotCopiedBytes); + }); + }); + + describe('readAddress', () => { + it('should successfully read address when the address takes up the whole array', async () => { + const byteArray = ethUtil.addHexPrefix(testAddress); + const testAddressOffset = new BigNumber(0); + const address = await libBytes.publicReadAddress.callAsync(byteArray, testAddressOffset); + return expect(address).to.be.equal(testAddress); + }); + it('should successfully read address when it is offset in the array', async () => { + const addressByteArrayBuffer = ethUtil.toBuffer(testAddress); + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, addressByteArrayBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testAddressOffset = new BigNumber(prefixByteArrayBuffer.byteLength); + const address = await libBytes.publicReadAddress.callAsync(combinedByteArray, testAddressOffset); + return expect(address).to.be.equal(testAddress); + }); + it('should fail if the byte array is too short to hold an address', async () => { + const shortByteArray = '0xabcdef'; + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicReadAddress.callAsync(shortByteArray, offset), + RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold an address', async () => { + const byteArray = testAddress; + const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); + return expectContractCallFailed( + libBytes.publicReadAddress.callAsync(byteArray, badOffset), + RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + ); + }); + }); + + describe('writeAddress', () => { + it('should successfully write address when the address takes up the whole array', async () => { + const byteArray = testAddress; + const testAddressOffset = new BigNumber(0); + const newByteArray = await libBytes.publicWriteAddress.callAsync( + byteArray, + testAddressOffset, + testAddressB, + ); + return expect(newByteArray).to.be.equal(testAddressB); + }); + it('should successfully write address when it is offset in the array', async () => { + const addressByteArrayBuffer = ethUtil.toBuffer(testAddress); + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, addressByteArrayBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testAddressOffset = new BigNumber(prefixByteArrayBuffer.byteLength); + const newByteArray = await libBytes.publicWriteAddress.callAsync( + combinedByteArray, + testAddressOffset, + testAddressB, + ); + const newByteArrayBuffer = ethUtil.toBuffer(newByteArray); + const addressFromOffsetBuffer = newByteArrayBuffer.slice(prefixByteArrayBuffer.byteLength); + const addressFromOffset = ethUtil.addHexPrefix(ethUtil.bufferToHex(addressFromOffsetBuffer)); + return expect(addressFromOffset).to.be.equal(testAddressB); + }); + it('should fail if the byte array is too short to hold an address', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicWriteAddress.callAsync(byteArrayShorterThan20Bytes, offset, testAddress), + RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold an address', async () => { + const byteArray = byteArrayLongerThan32Bytes; + const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); + return expectContractCallFailed( + libBytes.publicWriteAddress.callAsync(byteArray, badOffset, testAddress), + RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + ); + }); + }); + + describe('readBytes32', () => { + it('should successfully read bytes32 when the bytes32 takes up the whole array', async () => { + const testBytes32Offset = new BigNumber(0); + const bytes32 = await libBytes.publicReadBytes32.callAsync(testBytes32, testBytes32Offset); + return expect(bytes32).to.be.equal(testBytes32); + }); + it('should successfully read bytes32 when it is offset in the array)', async () => { + const bytes32ByteArrayBuffer = ethUtil.toBuffer(testBytes32); + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, bytes32ByteArrayBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testBytes32Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const bytes32 = await libBytes.publicReadBytes32.callAsync(combinedByteArray, testBytes32Offset); + return expect(bytes32).to.be.equal(testBytes32); + }); + it('should fail if the byte array is too short to hold a bytes32', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicReadBytes32.callAsync(byteArrayShorterThan32Bytes, offset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold a bytes32', async () => { + const badOffset = new BigNumber(ethUtil.toBuffer(testBytes32).byteLength); + return expectContractCallFailed( + libBytes.publicReadBytes32.callAsync(testBytes32, badOffset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + }); + + describe('writeBytes32', () => { + it('should successfully write bytes32 when the address takes up the whole array', async () => { + const byteArray = testBytes32; + const testBytes32Offset = new BigNumber(0); + const newByteArray = await libBytes.publicWriteBytes32.callAsync( + byteArray, + testBytes32Offset, + testBytes32B, + ); + return expect(newByteArray).to.be.equal(testBytes32B); + }); + it('should successfully write bytes32 when it is offset in the array', async () => { + const bytes32ByteArrayBuffer = ethUtil.toBuffer(testBytes32); + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, bytes32ByteArrayBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testBytes32Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const newByteArray = await libBytes.publicWriteBytes32.callAsync( + combinedByteArray, + testBytes32Offset, + testBytes32B, + ); + const newByteArrayBuffer = ethUtil.toBuffer(newByteArray); + const bytes32FromOffsetBuffer = newByteArrayBuffer.slice(prefixByteArrayBuffer.byteLength); + const bytes32FromOffset = ethUtil.addHexPrefix(ethUtil.bufferToHex(bytes32FromOffsetBuffer)); + return expect(bytes32FromOffset).to.be.equal(testBytes32B); + }); + it('should fail if the byte array is too short to hold a bytes32', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicWriteBytes32.callAsync(byteArrayShorterThan32Bytes, offset, testBytes32), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold a bytes32', async () => { + const byteArray = byteArrayLongerThan32Bytes; + const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); + return expectContractCallFailed( + libBytes.publicWriteBytes32.callAsync(byteArray, badOffset, testBytes32), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + }); + + describe('readUint256', () => { + it('should successfully read uint256 when the uint256 takes up the whole array', async () => { + const formattedTestUint256 = new BN(testUint256.toString(10)); + const testUint256AsBuffer = ethUtil.toBuffer(formattedTestUint256); + const byteArray = ethUtil.bufferToHex(testUint256AsBuffer); + const testUint256Offset = new BigNumber(0); + const uint256 = await libBytes.publicReadUint256.callAsync(byteArray, testUint256Offset); + return expect(uint256).to.bignumber.equal(testUint256); + }); + it('should successfully read uint256 when it is offset in the array', async () => { + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const formattedTestUint256 = new BN(testUint256.toString(10)); + const testUint256AsBuffer = ethUtil.toBuffer(formattedTestUint256); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, testUint256AsBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const uint256 = await libBytes.publicReadUint256.callAsync(combinedByteArray, testUint256Offset); + return expect(uint256).to.bignumber.equal(testUint256); + }); + it('should fail if the byte array is too short to hold a uint256', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicReadUint256.callAsync(byteArrayShorterThan32Bytes, offset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold a uint256', async () => { + const formattedTestUint256 = new BN(testUint256.toString(10)); + const testUint256AsBuffer = ethUtil.toBuffer(formattedTestUint256); + const byteArray = ethUtil.bufferToHex(testUint256AsBuffer); + const badOffset = new BigNumber(testUint256AsBuffer.byteLength); + return expectContractCallFailed( + libBytes.publicReadUint256.callAsync(byteArray, badOffset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + }); + + describe('writeUint256', () => { + it('should successfully write uint256 when the address takes up the whole array', async () => { + const byteArray = testBytes32; + const testUint256Offset = new BigNumber(0); + const newByteArray = await libBytes.publicWriteUint256.callAsync( + byteArray, + testUint256Offset, + testUint256B, + ); + const newByteArrayAsUint256 = new BigNumber(newByteArray, 16); + return expect(newByteArrayAsUint256).to.be.bignumber.equal(testUint256B); + }); + it('should successfully write uint256 when it is offset in the array', async () => { + const bytes32ByteArrayBuffer = ethUtil.toBuffer(testBytes32); + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, bytes32ByteArrayBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const newByteArray = await libBytes.publicWriteUint256.callAsync( + combinedByteArray, + testUint256Offset, + testUint256B, + ); + const newByteArrayBuffer = ethUtil.toBuffer(newByteArray); + const uint256FromOffsetBuffer = newByteArrayBuffer.slice(prefixByteArrayBuffer.byteLength); + const uint256FromOffset = new BigNumber( + ethUtil.addHexPrefix(ethUtil.bufferToHex(uint256FromOffsetBuffer)), + 16, + ); + return expect(uint256FromOffset).to.be.bignumber.equal(testUint256B); + }); + it('should fail if the byte array is too short to hold a uint256', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicWriteUint256.callAsync(byteArrayShorterThan32Bytes, offset, testUint256), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold a uint256', async () => { + const byteArray = byteArrayLongerThan32Bytes; + const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); + return expectContractCallFailed( + libBytes.publicWriteUint256.callAsync(byteArray, badOffset, testUint256), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + }); + + describe('readBytes4', () => { + // AssertionError: expected promise to be rejected with an error including 'revert' but it was fulfilled with '0x08c379a0' + it('should revert if byte array has a length < 4', async () => { + const byteArrayLessThan4Bytes = '0x010101'; + return expectContractCallFailed( + libBytes.publicReadBytes4.callAsync(byteArrayLessThan4Bytes, new BigNumber(0)), + RevertReason.LibBytesGreaterOrEqualTo4LengthRequired, + ); + }); + it('should return the first 4 bytes of a byte array of arbitrary length', async () => { + const first4Bytes = await libBytes.publicReadBytes4.callAsync(byteArrayLongerThan32Bytes, new BigNumber(0)); + const expectedFirst4Bytes = byteArrayLongerThan32Bytes.slice(0, 10); + expect(first4Bytes).to.equal(expectedFirst4Bytes); + }); + }); + + describe('readBytesWithLength', () => { + it('should successfully read short, nested array of bytes when it takes up the whole array', async () => { + const testBytesOffset = new BigNumber(0); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(shortTestBytes, testBytesOffset); + return expect(bytes).to.be.equal(shortData); + }); + it('should successfully read short, nested array of bytes when it is offset in the array', async () => { + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, shortTestBytesAsBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(combinedByteArray, testUint256Offset); + return expect(bytes).to.be.equal(shortData); + }); + it('should successfully read a nested array of bytes - one word in length - when it takes up the whole array', async () => { + const testBytesOffset = new BigNumber(0); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(wordOfTestBytes, testBytesOffset); + return expect(bytes).to.be.equal(wordOfData); + }); + it('should successfully read a nested array of bytes - one word in length - when it is offset in the array', async () => { + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, wordOfTestBytesAsBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(combinedByteArray, testUint256Offset); + return expect(bytes).to.be.equal(wordOfData); + }); + it('should successfully read long, nested array of bytes when it takes up the whole array', async () => { + const testBytesOffset = new BigNumber(0); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(longTestBytes, testBytesOffset); + return expect(bytes).to.be.equal(longData); + }); + it('should successfully read long, nested array of bytes when it is offset in the array', async () => { + const prefixByteArrayBuffer = ethUtil.toBuffer('0xabcdef'); + const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, longTestBytesAsBuffer]); + const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); + const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); + const bytes = await libBytes.publicReadBytesWithLength.callAsync(combinedByteArray, testUint256Offset); + return expect(bytes).to.be.equal(longData); + }); + it('should fail if the byte array is too short to hold the length of a nested byte array', async () => { + // The length of the nested array is 32 bytes. By storing less than 32 bytes, a length cannot be read. + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicReadBytesWithLength.callAsync(byteArrayShorterThan32Bytes, offset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if we store a nested byte array length, without a nested byte array', async () => { + const offset = new BigNumber(0); + return expectContractCallFailed( + libBytes.publicReadBytesWithLength.callAsync(testBytes32, offset), + RevertReason.LibBytesGreaterOrEqualToNestedBytesLengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold the length of a nested byte array', async () => { + const badOffset = new BigNumber(ethUtil.toBuffer(byteArrayShorterThan32Bytes).byteLength); + return expectContractCallFailed( + libBytes.publicReadBytesWithLength.callAsync(byteArrayShorterThan32Bytes, badOffset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold the nested byte array', async () => { + const badOffset = new BigNumber(ethUtil.toBuffer(testBytes32).byteLength); + return expectContractCallFailed( + libBytes.publicReadBytesWithLength.callAsync(testBytes32, badOffset), + RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + ); + }); + }); + + describe('writeBytesWithLength', () => { + it('should successfully write short, nested array of bytes when it takes up the whole array)', async () => { + const testBytesOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex(new Buffer(shortTestBytesAsBuffer.byteLength)); + const bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + testBytesOffset, + shortData, + ); + const bytesRead = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytesRead).to.be.equal(shortData); + }); + it('should successfully write short, nested array of bytes when it is offset in the array', async () => { + // Write a prefix to the array + const prefixData = '0xabcdef'; + const prefixDataAsBuffer = ethUtil.toBuffer(prefixData); + const prefixOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex( + new Buffer(prefixDataAsBuffer.byteLength + shortTestBytesAsBuffer.byteLength), + ); + let bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + prefixOffset, + prefixData, + ); + // Write data after prefix + const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength); + bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + bytesWritten, + testBytesOffset, + shortData, + ); + // Read data after prefix and validate + const bytes = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytes).to.be.equal(shortData); + }); + it('should successfully write a nested array of bytes - one word in length - when it takes up the whole array', async () => { + const testBytesOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex(new Buffer(wordOfTestBytesAsBuffer.byteLength)); + const bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + testBytesOffset, + wordOfData, + ); + const bytesRead = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytesRead).to.be.equal(wordOfData); + }); + it('should successfully write a nested array of bytes - one word in length - when it is offset in the array', async () => { + // Write a prefix to the array + const prefixData = '0xabcdef'; + const prefixDataAsBuffer = ethUtil.toBuffer(prefixData); + const prefixOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex( + new Buffer(prefixDataAsBuffer.byteLength + wordOfTestBytesAsBuffer.byteLength), + ); + let bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + prefixOffset, + prefixData, + ); + // Write data after prefix + const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength); + bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + bytesWritten, + testBytesOffset, + wordOfData, + ); + // Read data after prefix and validate + const bytes = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytes).to.be.equal(wordOfData); + }); + it('should successfully write a long, nested bytes when it takes up the whole array', async () => { + const testBytesOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex(new Buffer(longTestBytesAsBuffer.byteLength)); + const bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + testBytesOffset, + longData, + ); + const bytesRead = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytesRead).to.be.equal(longData); + }); + it('should successfully write long, nested array of bytes when it is offset in the array', async () => { + // Write a prefix to the array + const prefixData = '0xabcdef'; + const prefixDataAsBuffer = ethUtil.toBuffer(prefixData); + const prefixOffset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex( + new Buffer(prefixDataAsBuffer.byteLength + longTestBytesAsBuffer.byteLength), + ); + let bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync( + emptyByteArray, + prefixOffset, + prefixData, + ); + // Write data after prefix + const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength); + bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync(bytesWritten, testBytesOffset, longData); + // Read data after prefix and validate + const bytes = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset); + return expect(bytes).to.be.equal(longData); + }); + it('should fail if the byte array is too short to hold the length of a nested byte array', async () => { + const offset = new BigNumber(0); + const emptyByteArray = ethUtil.bufferToHex(new Buffer(1)); + return expectContractCallFailed( + libBytes.publicWriteBytesWithLength.callAsync(emptyByteArray, offset, longData), + RevertReason.LibBytesGreaterOrEqualToNestedBytesLengthRequired, + ); + }); + it('should fail if the length between the offset and end of the byte array is too short to hold the length of a nested byte array)', async () => { + const emptyByteArray = ethUtil.bufferToHex(new Buffer(shortTestBytesAsBuffer.byteLength)); + const badOffset = new BigNumber(ethUtil.toBuffer(shortTestBytesAsBuffer).byteLength); + return expectContractCallFailed( + libBytes.publicWriteBytesWithLength.callAsync(emptyByteArray, badOffset, shortData), + RevertReason.LibBytesGreaterOrEqualToNestedBytesLengthRequired, + ); + }); + }); + + describe('memCopy', () => { + // Create memory 0x000102...FF + const memSize = 256; + // tslint:disable:no-shadowed-variable + const memory = new Uint8Array(memSize).map((_, i) => i); + const memHex = toHex(memory); + + // Reference implementation to test against + const refMemcpy = (mem: Uint8Array, dest: number, source: number, length: number): Uint8Array => + Uint8Array.from(mem).copyWithin(dest, source, source + length); + + // Test vectors: destination, source, length, job description + type Tests = Array<[number, number, number, string]>; + + const test = (tests: Tests) => + tests.forEach(([dest, source, length, job]) => + it(job, async () => { + const expected = refMemcpy(memory, dest, source, length); + const resultStr = await libBytes.testMemcpy.callAsync( + memHex, + new BigNumber(dest), + new BigNumber(source), + new BigNumber(length), + ); + const result = fromHex(resultStr); + expect(result).to.deep.equal(expected); + }), + ); + + test([[0, 0, 0, 'copies zero bytes with overlap']]); + + describe('copies forward', () => + test([ + [128, 0, 0, 'zero bytes'], + [128, 0, 1, 'one byte'], + [128, 0, 11, 'eleven bytes'], + [128, 0, 31, 'thirty-one bytes'], + [128, 0, 32, 'one word'], + [128, 0, 64, 'two words'], + [128, 0, 96, 'three words'], + [128, 0, 33, 'one word and one byte'], + [128, 0, 72, 'two words and eight bytes'], + [128, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward within one word', () => + test([ + [16, 0, 0, 'zero bytes'], + [16, 0, 1, 'one byte'], + [16, 0, 11, 'eleven bytes'], + [16, 0, 16, 'sixteen bytes'], + ])); + + describe('copies forward with one byte overlap', () => + test([ + [0, 0, 1, 'one byte'], + [10, 0, 11, 'eleven bytes'], + [30, 0, 31, 'thirty-one bytes'], + [31, 0, 32, 'one word'], + [32, 0, 33, 'one word and one byte'], + [71, 0, 72, 'two words and eight bytes'], + [99, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward with thirty-one bytes overlap', () => + test([ + [0, 0, 31, 'thirty-one bytes'], + [1, 0, 32, 'one word'], + [2, 0, 33, 'one word and one byte'], + [41, 0, 72, 'two words and eight bytes'], + [69, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward with one word overlap', () => + test([ + [0, 0, 32, 'one word'], + [1, 0, 33, 'one word and one byte'], + [41, 0, 72, 'two words and eight bytes'], + [69, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward with one word and one byte overlap', () => + test([ + [0, 0, 33, 'one word and one byte'], + [40, 0, 72, 'two words and eight bytes'], + [68, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward with two words overlap', () => + test([ + [0, 0, 64, 'two words'], + [8, 0, 72, 'two words and eight bytes'], + [36, 0, 100, 'three words and four bytes'], + ])); + + describe('copies forward within one word and one byte overlap', () => + test([[0, 0, 1, 'one byte'], [10, 0, 11, 'eleven bytes'], [15, 0, 16, 'sixteen bytes']])); + + describe('copies backward', () => + test([ + [0, 128, 0, 'zero bytes'], + [0, 128, 1, 'one byte'], + [0, 128, 11, 'eleven bytes'], + [0, 128, 31, 'thirty-one bytes'], + [0, 128, 32, 'one word'], + [0, 128, 64, 'two words'], + [0, 128, 96, 'three words'], + [0, 128, 33, 'one word and one byte'], + [0, 128, 72, 'two words and eight bytes'], + [0, 128, 100, 'three words and four bytes'], + ])); + + describe('copies backward within one word', () => + test([ + [0, 16, 0, 'zero bytes'], + [0, 16, 1, 'one byte'], + [0, 16, 11, 'eleven bytes'], + [0, 16, 16, 'sixteen bytes'], + ])); + + describe('copies backward with one byte overlap', () => + test([ + [0, 0, 1, 'one byte'], + [0, 10, 11, 'eleven bytes'], + [0, 30, 31, 'thirty-one bytes'], + [0, 31, 32, 'one word'], + [0, 32, 33, 'one word and one byte'], + [0, 71, 72, 'two words and eight bytes'], + [0, 99, 100, 'three words and four bytes'], + ])); + + describe('copies backward with thirty-one bytes overlap', () => + test([ + [0, 0, 31, 'thirty-one bytes'], + [0, 1, 32, 'one word'], + [0, 2, 33, 'one word and one byte'], + [0, 41, 72, 'two words and eight bytes'], + [0, 69, 100, 'three words and four bytes'], + ])); + + describe('copies backward with one word overlap', () => + test([ + [0, 0, 32, 'one word'], + [0, 1, 33, 'one word and one byte'], + [0, 41, 72, 'two words and eight bytes'], + [0, 69, 100, 'three words and four bytes'], + ])); + + describe('copies backward with one word and one byte overlap', () => + test([ + [0, 0, 33, 'one word and one byte'], + [0, 40, 72, 'two words and eight bytes'], + [0, 68, 100, 'three words and four bytes'], + ])); + + describe('copies backward with two words overlap', () => + test([ + [0, 0, 64, 'two words'], + [0, 8, 72, 'two words and eight bytes'], + [0, 36, 100, 'three words and four bytes'], + ])); + + describe('copies forward within one word and one byte overlap', () => + test([[0, 0, 1, 'one byte'], [0, 10, 11, 'eleven bytes'], [0, 15, 16, 'sixteen bytes']])); + }); +}); +// tslint:disable:max-file-line-count diff --git a/packages/contracts/test/multi_sig_with_time_lock.ts b/packages/contracts/test/multi_sig_with_time_lock.ts deleted file mode 100644 index d35efe9da..000000000 --- a/packages/contracts/test/multi_sig_with_time_lock.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { LogWithDecodedArgs, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, web3Factory } from '@0xproject/dev-utils'; -import { AbiDecoder, BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import * as multiSigWalletJSON from '../../build/contracts/MultiSigWalletWithTimeLock.json'; -import { MultiSigWalletContract } from '../src/contract_wrappers/generated/multi_sig_wallet'; -import { MultiSigWalletWithTimeLockContract } from '../src/contract_wrappers/generated/multi_sig_wallet_with_time_lock'; -import { artifacts } from '../util/artifacts'; -import { constants } from '../util/constants'; -import { MultiSigWrapper } from '../util/multi_sig_wrapper'; -import { ContractName, SubmissionContractEventArgs } from '../util/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; - -const MULTI_SIG_ABI = artifacts.MultiSigWalletWithTimeLockArtifact.networks[constants.TESTRPC_NETWORK_ID].abi; -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -const zeroEx = new ZeroEx(web3.currentProvider, { networkId: constants.TESTRPC_NETWORK_ID }); -const abiDecoder = new AbiDecoder([MULTI_SIG_ABI]); - -describe('MultiSigWalletWithTimeLock', () => { - let owners: string[]; - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - owners = [accounts[0], accounts[1]]; - }); - const SIGNATURES_REQUIRED = new BigNumber(2); - const SECONDS_TIME_LOCKED = new BigNumber(10000); - - let multiSig: MultiSigWalletWithTimeLockContract; - let multiSigWrapper: MultiSigWrapper; - let txId: BigNumber; - let initialSecondsTimeLocked: number; - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - - describe('changeTimeLock', () => { - describe('initially non-time-locked', async () => { - before('deploy a walet', async () => { - const multiSigInstance = await deployer.deployAsync(ContractName.MultiSigWalletWithTimeLock, [ - owners, - SIGNATURES_REQUIRED, - 0, - ]); - multiSig = new MultiSigWalletWithTimeLockContract( - web3Wrapper, - multiSigInstance.abi, - multiSigInstance.address, - ); - multiSigWrapper = new MultiSigWrapper((multiSig as any) as MultiSigWalletContract); - - const secondsTimeLocked = await multiSig.secondsTimeLocked.callAsync(); - initialSecondsTimeLocked = secondsTimeLocked.toNumber(); - }); - it('should throw when not called by wallet', async () => { - return expect( - multiSig.changeTimeLock.sendTransactionAsync(SECONDS_TIME_LOCKED, { from: owners[0] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw without enough confirmations', async () => { - const destination = multiSig.address; - const from = owners[0]; - const dataParams = { - name: 'changeTimeLock', - abi: MULTI_SIG_ABI, - args: [SECONDS_TIME_LOCKED.toNumber()], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(destination, from, dataParams); - const subRes = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(subRes.logs[0]) as LogWithDecodedArgs< - SubmissionContractEventArgs - >; - - txId = log.args.transactionId; - return expect( - multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should set confirmation time with enough confirmations', async () => { - const destination = multiSig.address; - const from = owners[0]; - const dataParams = { - name: 'changeTimeLock', - abi: MULTI_SIG_ABI, - args: [SECONDS_TIME_LOCKED.toNumber()], - }; - let txHash = await multiSigWrapper.submitTransactionAsync(destination, from, dataParams); - const subRes = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(subRes.logs[0]) as LogWithDecodedArgs< - SubmissionContractEventArgs - >; - - txId = log.args.transactionId; - txHash = await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - expect(res.logs).to.have.length(2); - - const blockNum = await web3Wrapper.getBlockNumberAsync(); - const blockInfo = await web3Wrapper.getBlockAsync(blockNum); - const timestamp = new BigNumber(blockInfo.timestamp); - const confirmationTimeBigNum = new BigNumber(await multiSig.confirmationTimes.callAsync(txId)); - - expect(timestamp).to.be.bignumber.equal(confirmationTimeBigNum); - }); - - it('should be executable with enough confirmations and secondsTimeLocked of 0', async () => { - const destination = multiSig.address; - const from = owners[0]; - const dataParams = { - name: 'changeTimeLock', - abi: MULTI_SIG_ABI, - args: [SECONDS_TIME_LOCKED.toNumber()], - }; - let txHash = await multiSigWrapper.submitTransactionAsync(destination, from, dataParams); - const subRes = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(subRes.logs[0]) as LogWithDecodedArgs< - SubmissionContractEventArgs - >; - - txId = log.args.transactionId; - txHash = await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - - expect(initialSecondsTimeLocked).to.be.equal(0); - - txHash = await multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - expect(res.logs).to.have.length(2); - - const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync()); - expect(secondsTimeLocked).to.be.bignumber.equal(SECONDS_TIME_LOCKED); - }); - }); - describe('initially time-locked', async () => { - before('deploy a walet', async () => { - const multiSigInstance = await deployer.deployAsync(ContractName.MultiSigWalletWithTimeLock, [ - owners, - SIGNATURES_REQUIRED, - SECONDS_TIME_LOCKED, - ]); - multiSig = new MultiSigWalletWithTimeLockContract( - web3Wrapper, - multiSigInstance.abi, - multiSigInstance.address, - ); - multiSigWrapper = new MultiSigWrapper((multiSig as any) as MultiSigWalletContract); - - const secondsTimeLocked = await multiSig.secondsTimeLocked.callAsync(); - initialSecondsTimeLocked = secondsTimeLocked.toNumber(); - const destination = multiSig.address; - const from = owners[0]; - const dataParams = { - name: 'changeTimeLock', - abi: MULTI_SIG_ABI, - args: [newSecondsTimeLocked], - }; - let txHash = await multiSigWrapper.submitTransactionAsync(destination, from, dataParams); - const subRes = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(subRes.logs[0]) as LogWithDecodedArgs< - SubmissionContractEventArgs - >; - txId = log.args.transactionId; - txHash = await multiSig.confirmTransaction.sendTransactionAsync(txId, { - from: owners[1], - }); - const confRes = await zeroEx.awaitTransactionMinedAsync(txHash); - expect(confRes.logs).to.have.length(2); - }); - const newSecondsTimeLocked = 0; - it('should throw if it has enough confirmations but is not past the time lock', async () => { - return expect( - multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should execute if it has enough confirmations and is past the time lock', async () => { - await web3Wrapper.increaseTimeAsync(SECONDS_TIME_LOCKED.toNumber()); - await multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }); - - const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync()); - expect(secondsTimeLocked).to.be.bignumber.equal(newSecondsTimeLocked); - }); - }); - }); -}); diff --git a/packages/contracts/test/multi_sig_with_time_lock_except_remove_auth_addr.ts b/packages/contracts/test/multi_sig_with_time_lock_except_remove_auth_addr.ts deleted file mode 100644 index 6d20a67f3..000000000 --- a/packages/contracts/test/multi_sig_with_time_lock_except_remove_auth_addr.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { LogWithDecodedArgs, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { AbiDecoder } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import { MultiSigWalletContract } from '../src/contract_wrappers/generated/multi_sig_wallet'; -import { MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressContract } from '../src/contract_wrappers/generated/multi_sig_wallet_with_time_lock_except_remove_authorized_address'; -import { TokenTransferProxyContract } from '../src/contract_wrappers/generated/token_transfer_proxy'; -import { artifacts } from '../util/artifacts'; -import { constants } from '../util/constants'; -import { crypto } from '../util/crypto'; -import { MultiSigWrapper } from '../util/multi_sig_wrapper'; -import { ContractName, SubmissionContractEventArgs, TransactionDataParams } from '../util/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; -const PROXY_ABI = artifacts.TokenTransferProxyArtifact.networks[constants.TESTRPC_NETWORK_ID].abi; -const MUTISIG_WALLET_WITH_TIME_LOCK_EXCEPT_REMOVE_AUTHORIZED_ADDRESS_ABI = - artifacts.MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressArtifact.networks[constants.TESTRPC_NETWORK_ID] - .abi; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -const abiDecoder = new AbiDecoder([MUTISIG_WALLET_WITH_TIME_LOCK_EXCEPT_REMOVE_AUTHORIZED_ADDRESS_ABI]); - -describe('MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress', () => { - const zeroEx = new ZeroEx(web3.currentProvider, { networkId: constants.TESTRPC_NETWORK_ID }); - let owners: string[]; - const requiredApprovals = 2; - const SECONDS_TIME_LOCKED = 1000000; - - // initialize fake addresses - let authorizedAddress: string; - let unauthorizedAddress: string; - - let tokenTransferProxy: TokenTransferProxyContract; - let multiSig: MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressContract; - let multiSigWrapper: MultiSigWrapper; - - let validDestination: string; - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - owners = [accounts[0], accounts[1]]; - [authorizedAddress, unauthorizedAddress] = accounts; - const initialOwner = accounts[0]; - const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); - tokenTransferProxy = new TokenTransferProxyContract( - web3Wrapper, - tokenTransferProxyInstance.abi, - tokenTransferProxyInstance.address, - ); - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(authorizedAddress, { - from: initialOwner, - }); - const multiSigInstance = await deployer.deployAsync( - ContractName.MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress, - [owners, requiredApprovals, SECONDS_TIME_LOCKED, tokenTransferProxy.address], - ); - multiSig = new MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressContract( - web3Wrapper, - multiSigInstance.abi, - multiSigInstance.address, - ); - await tokenTransferProxy.transferOwnership.sendTransactionAsync(multiSig.address, { - from: initialOwner, - }); - multiSigWrapper = new MultiSigWrapper((multiSig as any) as MultiSigWalletContract); - validDestination = tokenTransferProxy.address; - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - - describe('isFunctionRemoveAuthorizedAddress', () => { - it('should throw if data is not for removeAuthorizedAddress', async () => { - const data = MultiSigWrapper.encodeFnArgs('addAuthorizedAddress', PROXY_ABI, [owners[0]]); - return expect(multiSig.isFunctionRemoveAuthorizedAddress.callAsync(data)).to.be.rejectedWith( - constants.REVERT, - ); - }); - - it('should return true if data is for removeAuthorizedAddress', async () => { - const data = MultiSigWrapper.encodeFnArgs('removeAuthorizedAddress', PROXY_ABI, [owners[0]]); - const isFunctionRemoveAuthorizedAddress = await multiSig.isFunctionRemoveAuthorizedAddress.callAsync(data); - expect(isFunctionRemoveAuthorizedAddress).to.be.true(); - }); - }); - - describe('executeRemoveAuthorizedAddress', () => { - it('should throw without the required confirmations', async () => { - const dataParams: TransactionDataParams = { - name: 'removeAuthorizedAddress', - abi: PROXY_ABI, - args: [authorizedAddress], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(validDestination, owners[0], dataParams); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(res.logs[0]) as LogWithDecodedArgs<SubmissionContractEventArgs>; - const txId = log.args.transactionId; - - return expect( - multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if tx destination is not the tokenTransferProxy', async () => { - const invalidTokenTransferProxy = await deployer.deployAsync(ContractName.TokenTransferProxy); - const invalidDestination = invalidTokenTransferProxy.address; - const dataParams: TransactionDataParams = { - name: 'removeAuthorizedAddress', - abi: PROXY_ABI, - args: [authorizedAddress], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(invalidDestination, owners[0], dataParams); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(res.logs[0]) as LogWithDecodedArgs<SubmissionContractEventArgs>; - const txId = log.args.transactionId; - await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - const isConfirmed = await multiSig.isConfirmed.callAsync(txId); - expect(isConfirmed).to.be.true(); - - return expect( - multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if tx data is not for removeAuthorizedAddress', async () => { - const dataParams: TransactionDataParams = { - name: 'addAuthorizedAddress', - abi: PROXY_ABI, - args: [unauthorizedAddress], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(validDestination, owners[0], dataParams); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(res.logs[0]) as LogWithDecodedArgs<SubmissionContractEventArgs>; - const txId = log.args.transactionId; - await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - const isConfirmed = await multiSig.isConfirmed.callAsync(txId); - expect(isConfirmed).to.be.true(); - - return expect( - multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should execute removeAuthorizedAddress for valid tokenTransferProxy if fully confirmed', async () => { - const dataParams: TransactionDataParams = { - name: 'removeAuthorizedAddress', - abi: PROXY_ABI, - args: [authorizedAddress], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(validDestination, owners[0], dataParams); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(res.logs[0]) as LogWithDecodedArgs<SubmissionContractEventArgs>; - const txId = log.args.transactionId; - await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - const isConfirmed = await multiSig.isConfirmed.callAsync(txId); - expect(isConfirmed).to.be.true(); - await multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }); - const isAuthorized = await tokenTransferProxy.authorized.callAsync(authorizedAddress); - expect(isAuthorized).to.be.false(); - }); - - it('should throw if already executed', async () => { - const dataParams: TransactionDataParams = { - name: 'removeAuthorizedAddress', - abi: PROXY_ABI, - args: [authorizedAddress], - }; - const txHash = await multiSigWrapper.submitTransactionAsync(validDestination, owners[0], dataParams); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const log = abiDecoder.tryToDecodeLogOrNoop(res.logs[0]) as LogWithDecodedArgs<SubmissionContractEventArgs>; - const txId = log.args.transactionId; - await multiSig.confirmTransaction.sendTransactionAsync(txId, { from: owners[1] }); - const isConfirmed = await multiSig.isConfirmed.callAsync(txId); - expect(isConfirmed).to.be.true(); - await multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }); - const tx = await multiSig.transactions.callAsync(txId); - const isExecuted = tx[3]; - expect(isExecuted).to.be.true(); - return expect( - multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), - ).to.be.rejectedWith(constants.REVERT); - }); - }); -}); diff --git a/packages/contracts/test/multisig/asset_proxy_owner.ts b/packages/contracts/test/multisig/asset_proxy_owner.ts new file mode 100644 index 000000000..6b98605d3 --- /dev/null +++ b/packages/contracts/test/multisig/asset_proxy_owner.ts @@ -0,0 +1,468 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; + +import { + AssetProxyOwnerAssetProxyRegistrationEventArgs, + AssetProxyOwnerContract, + AssetProxyOwnerExecutionEventArgs, + AssetProxyOwnerExecutionFailureEventArgs, + AssetProxyOwnerSubmissionEventArgs, +} from '../../generated_contract_wrappers/asset_proxy_owner'; +import { MixinAuthorizableContract } from '../../generated_contract_wrappers/mixin_authorizable'; +import { TestAssetProxyOwnerContract } from '../../generated_contract_wrappers/test_asset_proxy_owner'; +import { artifacts } from '../utils/artifacts'; +import { + expectContractCallFailedWithoutReasonAsync, + expectContractCreationFailedWithoutReason, + expectTransactionFailedWithoutReasonAsync, +} from '../utils/assertions'; +import { increaseTimeAndMineBlockAsync } from '../utils/block_timestamp'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { MultiSigWrapper } from '../utils/multi_sig_wrapper'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_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; + const REQUIRED_APPROVALS = new BigNumber(2); + const SECONDS_TIME_LOCKED = new BigNumber(1000000); + + let erc20Proxy: MixinAuthorizableContract; + let erc721Proxy: MixinAuthorizableContract; + let testAssetProxyOwner: TestAssetProxyOwnerContract; + let multiSigWrapper: MultiSigWrapper; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owners = [accounts[0], accounts[1]]; + const initialOwner = (authorized = 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, + ); + multiSigWrapper = new MultiSigWrapper(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 expectContractCreationFailedWithoutReason( + AssetProxyOwnerContract.deployFrom0xArtifactAsync( + artifacts.AssetProxyOwner, + provider, + txDefaults, + owners, + assetProxyContractAddresses, + REQUIRED_APPROVALS, + SECONDS_TIME_LOCKED, + ), + ); + }); + }); + + 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('readBytes4', () => { + it('should revert if byte array has a length < 4', async () => { + const byteArrayLessThan4Bytes = '0x010101'; + return expectContractCallFailedWithoutReasonAsync( + testAssetProxyOwner.publicReadBytes4.callAsync(byteArrayLessThan4Bytes, new BigNumber(0)), + ); + }); + it('should return the first 4 bytes of a byte array of arbitrary length', async () => { + const byteArrayLongerThan32Bytes = + '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const first4Bytes = await testAssetProxyOwner.publicReadBytes4.callAsync( + byteArrayLongerThan32Bytes, + new BigNumber(0), + ); + const expectedFirst4Bytes = byteArrayLongerThan32Bytes.slice(0, 10); + expect(first4Bytes).to.equal(expectedFirst4Bytes); + }); + }); + + 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 multiSigWrapper.submitTransactionAsync( + testAssetProxyOwner.address, + registerAssetProxyData, + owners[0], + ); + + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); + + const executeTxRes = await multiSigWrapper.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 multiSigWrapper.submitTransactionAsync( + testAssetProxyOwner.address, + registerAssetProxyData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); + + const executeTxRes = await multiSigWrapper.executeTransactionAsync(txId, owners[0]); + const failureLog = executeTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerExecutionFailureEventArgs>; + expect(failureLog.args.transactionId).to.be.bignumber.equal(txId); + + const isAssetProxyRegistered = await testAssetProxyOwner.isAssetProxyRegistered.callAsync( + addressToRegister, + ); + expect(isAssetProxyRegistered).to.equal(false); + }); + }); + + describe('Calling removeAuthorizedAddressAtIndex', () => { + const erc20Index = new BigNumber(0); + const erc721Index = new BigNumber(1); + before('authorize both proxies and register erc20 proxy', async () => { + // Only register ERC20 proxy + const addressToRegister = erc20Proxy.address; + const isRegistered = true; + const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData( + addressToRegister, + isRegistered, + ); + const registerAssetProxySubmitRes = await multiSigWrapper.submitTransactionAsync( + testAssetProxyOwner.address, + registerAssetProxyData, + owners[0], + ); + const registerAssetProxySubmitLog = registerAssetProxySubmitRes.logs[0] as LogWithDecodedArgs< + AssetProxyOwnerSubmissionEventArgs + >; + + const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(authorized); + const erc20AddAuthorizedAddressSubmitRes = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + addAuthorizedAddressData, + owners[0], + ); + const erc721AddAuthorizedAddressSubmitRes = await multiSigWrapper.submitTransactionAsync( + erc721Proxy.address, + addAuthorizedAddressData, + owners[0], + ); + const erc20AddAuthorizedAddressSubmitLog = erc20AddAuthorizedAddressSubmitRes.logs[0] as LogWithDecodedArgs< + AssetProxyOwnerSubmissionEventArgs + >; + const erc721AddAuthorizedAddressSubmitLog = erc721AddAuthorizedAddressSubmitRes + .logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + + const registerAssetProxyTxId = registerAssetProxySubmitLog.args.transactionId; + const erc20AddAuthorizedAddressTxId = erc20AddAuthorizedAddressSubmitLog.args.transactionId; + const erc721AddAuthorizedAddressTxId = erc721AddAuthorizedAddressSubmitLog.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(registerAssetProxyTxId, owners[1]); + await multiSigWrapper.confirmTransactionAsync(erc20AddAuthorizedAddressTxId, owners[1]); + await multiSigWrapper.confirmTransactionAsync(erc721AddAuthorizedAddressTxId, owners[1]); + await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); + await multiSigWrapper.executeTransactionAsync(registerAssetProxyTxId, owners[0]); + await multiSigWrapper.executeTransactionAsync(erc20AddAuthorizedAddressTxId, owners[0]); + await multiSigWrapper.executeTransactionAsync(erc721AddAuthorizedAddressTxId, owners[0]); + }); + + 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 multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + notRemoveAuthorizedAddressData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + return expectContractCallFailedWithoutReasonAsync( + testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId), + ); + }); + + it('should return true if data is for removeAuthorizedAddressAtIndex and proxy is registered', async () => { + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const submitTxRes = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + const isValidRemoveAuthorizedAddressAtIndexTx = await testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync( + txId, + ); + expect(isValidRemoveAuthorizedAddressAtIndexTx).to.be.true(); + }); + + it('should revert if data is for removeAuthorizedAddressAtIndex and proxy is not registered', async () => { + const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc721Index, + ); + const submitTxRes = await multiSigWrapper.submitTransactionAsync( + erc721Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + return expectContractCallFailedWithoutReasonAsync( + testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId), + ); + }); + }); + + describe('executeRemoveAuthorizedAddressAtIndex', () => { + it('should throw without the required confirmations', async () => { + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const res = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + return expectTransactionFailedWithoutReasonAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + ); + }); + + it('should throw if tx destination is not registered', async () => { + const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc721Index, + ); + const res = await multiSigWrapper.submitTransactionAsync( + erc721Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + + return expectTransactionFailedWithoutReasonAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + ); + }); + + it('should throw if tx data is not for removeAuthorizedAddressAtIndex', async () => { + const newAuthorized = owners[1]; + const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData( + newAuthorized, + ); + const res = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + addAuthorizedAddressData, + owners[0], + ); + const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + + return expectTransactionFailedWithoutReasonAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + ); + }); + + it('should execute removeAuthorizedAddressAtIndex for registered address if fully confirmed', async () => { + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const submitRes = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = submitLog.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + + const execRes = await multiSigWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, owners[0]); + const execLog = execRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>; + expect(execLog.args.transactionId).to.be.bignumber.equal(txId); + + const tx = await testAssetProxyOwner.transactions.callAsync(txId); + const isExecuted = tx[3]; + expect(isExecuted).to.equal(true); + + const isAuthorized = await erc20Proxy.authorized.callAsync(authorized); + expect(isAuthorized).to.equal(false); + }); + + it('should throw if already executed', async () => { + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const submitRes = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = submitLog.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + + const execRes = await multiSigWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, owners[0]); + const execLog = execRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>; + expect(execLog.args.transactionId).to.be.bignumber.equal(txId); + + const tx = await testAssetProxyOwner.transactions.callAsync(txId); + const isExecuted = tx[3]; + expect(isExecuted).to.equal(true); + + return expectTransactionFailedWithoutReasonAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + ); + }); + }); + }); +}); +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/multisig/multi_sig_with_time_lock.ts b/packages/contracts/test/multisig/multi_sig_with_time_lock.ts new file mode 100644 index 000000000..8eeeeca6b --- /dev/null +++ b/packages/contracts/test/multisig/multi_sig_with_time_lock.ts @@ -0,0 +1,168 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; + +import { + MultiSigWalletWithTimeLockContract, + MultiSigWalletWithTimeLockSubmissionEventArgs, +} from '../../generated_contract_wrappers/multi_sig_wallet_with_time_lock'; +import { artifacts } from '../utils/artifacts'; +import { expectTransactionFailedWithoutReasonAsync } from '../utils/assertions'; +import { increaseTimeAndMineBlockAsync } from '../utils/block_timestamp'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { MultiSigWrapper } from '../utils/multi_sig_wrapper'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +// tslint:disable:no-unnecessary-type-assertion +describe('MultiSigWalletWithTimeLock', () => { + let owners: string[]; + const REQUIRED_APPROVALS = new BigNumber(2); + const SECONDS_TIME_LOCKED = new BigNumber(1000000); + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owners = [accounts[0], accounts[1]]; + }); + + let multiSig: MultiSigWalletWithTimeLockContract; + let multiSigWrapper: MultiSigWrapper; + + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('changeTimeLock', () => { + describe('initially non-time-locked', async () => { + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before('deploy a wallet', async () => { + const secondsTimeLocked = new BigNumber(0); + multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync( + artifacts.MultiSigWalletWithTimeLock, + provider, + txDefaults, + owners, + REQUIRED_APPROVALS, + secondsTimeLocked, + ); + multiSigWrapper = new MultiSigWrapper(multiSig, provider); + }); + + it('should throw when not called by wallet', async () => { + return expectTransactionFailedWithoutReasonAsync( + multiSig.changeTimeLock.sendTransactionAsync(SECONDS_TIME_LOCKED, { from: owners[0] }), + ); + }); + + it('should throw without enough confirmations', async () => { + const destination = multiSig.address; + const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED); + const res = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]); + const log = res.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>; + const txId = log.args.transactionId; + return expectTransactionFailedWithoutReasonAsync( + multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), + ); + }); + + it('should set confirmation time with enough confirmations', async () => { + const destination = multiSig.address; + const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED); + const subRes = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]); + const subLog = subRes.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>; + const txId = subLog.args.transactionId; + + const confirmRes = await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + expect(confirmRes.logs).to.have.length(2); + + const blockNum = await web3Wrapper.getBlockNumberAsync(); + const blockInfo = await web3Wrapper.getBlockAsync(blockNum); + const timestamp = new BigNumber(blockInfo.timestamp); + const confirmationTimeBigNum = new BigNumber(await multiSig.confirmationTimes.callAsync(txId)); + + expect(timestamp).to.be.bignumber.equal(confirmationTimeBigNum); + }); + + it('should be executable with enough confirmations and secondsTimeLocked of 0', async () => { + const destination = multiSig.address; + const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED); + const subRes = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]); + const subLog = subRes.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>; + const txId = subLog.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await multiSigWrapper.executeTransactionAsync(txId, owners[1]); + + const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync()); + expect(secondsTimeLocked).to.be.bignumber.equal(SECONDS_TIME_LOCKED); + }); + }); + describe('initially time-locked', async () => { + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + let txId: BigNumber; + const newSecondsTimeLocked = new BigNumber(0); + before('deploy a wallet, submit transaction to change timelock, and confirm the transaction', async () => { + multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync( + artifacts.MultiSigWalletWithTimeLock, + provider, + txDefaults, + owners, + REQUIRED_APPROVALS, + SECONDS_TIME_LOCKED, + ); + multiSigWrapper = new MultiSigWrapper(multiSig, provider); + + const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(newSecondsTimeLocked); + const res = await multiSigWrapper.submitTransactionAsync( + multiSig.address, + changeTimeLockData, + owners[0], + ); + const log = res.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>; + txId = log.args.transactionId; + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + }); + + it('should throw if it has enough confirmations but is not past the time lock', async () => { + return expectTransactionFailedWithoutReasonAsync( + multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), + ); + }); + + it('should execute if it has enough confirmations and is past the time lock', async () => { + await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); + await web3Wrapper.awaitTransactionSuccessAsync( + await multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync()); + expect(secondsTimeLocked).to.be.bignumber.equal(newSecondsTimeLocked); + }); + }); + }); +}); +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/token_registry.ts b/packages/contracts/test/token_registry.ts index 9dcc77b82..7cc43be9b 100644 --- a/packages/contracts/test/token_registry.ts +++ b/packages/contracts/test/token_registry.ts @@ -1,20 +1,17 @@ -import { ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber, NULL_BYTES } from '@0xproject/utils'; import * as chai from 'chai'; import ethUtil = require('ethereumjs-util'); import * as _ from 'lodash'; -import * as Web3 from 'web3'; -import { TokenRegistryContract } from '../src/contract_wrappers/generated/token_registry'; -import { constants } from '../util/constants'; -import { TokenRegWrapper } from '../util/token_registry_wrapper'; -import { ContractName } from '../util/types'; +import { TokenRegistryContract } from '../generated_contract_wrappers/token_registry'; +import { artifacts } from './utils/artifacts'; +import { expectTransactionFailedWithoutReasonAsync } from './utils/assertions'; import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; +import { constants } from './utils/constants'; +import { TokenRegWrapper } from './utils/token_registry_wrapper'; +import { provider, txDefaults, web3Wrapper } from './utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; @@ -25,13 +22,19 @@ describe('TokenRegistry', () => { let notOwner: string; let tokenReg: TokenRegistryContract; let tokenRegWrapper: TokenRegWrapper; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); owner = accounts[0]; notOwner = accounts[1]; - const tokenRegInstance = await deployer.deployAsync(ContractName.TokenRegistry); - tokenReg = new TokenRegistryContract(web3Wrapper, tokenRegInstance.abi, tokenRegInstance.address); - tokenRegWrapper = new TokenRegWrapper(tokenReg); + tokenReg = await TokenRegistryContract.deployFrom0xArtifactAsync(artifacts.TokenRegistry, provider, txDefaults); + tokenRegWrapper = new TokenRegWrapper(tokenReg, provider); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); @@ -62,17 +65,17 @@ describe('TokenRegistry', () => { }; const nullToken = { - address: ZeroEx.NULL_ADDRESS, + address: constants.NULL_ADDRESS, name: '', symbol: '', decimals: 0, - ipfsHash: constants.NULL_BYTES, - swarmHash: constants.NULL_BYTES, + ipfsHash: NULL_BYTES, + swarmHash: NULL_BYTES, }; describe('addToken', () => { it('should throw when not called by owner', async () => { - return expect(tokenRegWrapper.addTokenAsync(token1, notOwner)).to.be.rejectedWith(constants.REVERT); + return expectTransactionFailedWithoutReasonAsync(tokenRegWrapper.addTokenAsync(token1, notOwner)); }); it('should add token metadata when called by owner', async () => { @@ -84,20 +87,18 @@ describe('TokenRegistry', () => { it('should throw if token already exists', async () => { await tokenRegWrapper.addTokenAsync(token1, owner); - return expect(tokenRegWrapper.addTokenAsync(token1, owner)).to.be.rejectedWith(constants.REVERT); + return expectTransactionFailedWithoutReasonAsync(tokenRegWrapper.addTokenAsync(token1, owner)); }); it('should throw if token address is null', async () => { - return expect(tokenRegWrapper.addTokenAsync(nullToken, owner)).to.be.rejectedWith(constants.REVERT); + return expectTransactionFailedWithoutReasonAsync(tokenRegWrapper.addTokenAsync(nullToken, owner)); }); it('should throw if name already exists', async () => { await tokenRegWrapper.addTokenAsync(token1, owner); const duplicateNameToken = _.assign({}, token2, { name: token1.name }); - return expect(tokenRegWrapper.addTokenAsync(duplicateNameToken, owner)).to.be.rejectedWith( - constants.REVERT, - ); + return expectTransactionFailedWithoutReasonAsync(tokenRegWrapper.addTokenAsync(duplicateNameToken, owner)); }); it('should throw if symbol already exists', async () => { @@ -106,8 +107,8 @@ describe('TokenRegistry', () => { symbol: token1.symbol, }); - return expect(tokenRegWrapper.addTokenAsync(duplicateSymbolToken, owner)).to.be.rejectedWith( - constants.REVERT, + return expectTransactionFailedWithoutReasonAsync( + tokenRegWrapper.addTokenAsync(duplicateSymbolToken, owner), ); }); }); @@ -133,15 +134,18 @@ describe('TokenRegistry', () => { describe('setTokenName', () => { it('should throw when not called by owner', async () => { - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenName.sendTransactionAsync(token1.address, token2.name, { from: notOwner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should change the token name when called by owner', async () => { - await tokenReg.setTokenName.sendTransactionAsync(token1.address, token2.name, { - from: owner, - }); + await web3Wrapper.awaitTransactionSuccessAsync( + await tokenReg.setTokenName.sendTransactionAsync(token1.address, token2.name, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); const [newData, oldData] = await Promise.all([ tokenRegWrapper.getTokenByNameAsync(token2.name), tokenRegWrapper.getTokenByNameAsync(token1.name), @@ -156,29 +160,32 @@ describe('TokenRegistry', () => { it('should throw if the name already exists', async () => { await tokenRegWrapper.addTokenAsync(token2, owner); - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenName.sendTransactionAsync(token1.address, token2.name, { from: owner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should throw if token does not exist', async () => { - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenName.sendTransactionAsync(nullToken.address, token2.name, { from: owner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); }); describe('setTokenSymbol', () => { it('should throw when not called by owner', async () => { - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenSymbol.sendTransactionAsync(token1.address, token2.symbol, { from: notOwner, }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should change the token symbol when called by owner', async () => { - await tokenReg.setTokenSymbol.sendTransactionAsync(token1.address, token2.symbol, { from: owner }); + await web3Wrapper.awaitTransactionSuccessAsync( + await tokenReg.setTokenSymbol.sendTransactionAsync(token1.address, token2.symbol, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); const [newData, oldData] = await Promise.all([ tokenRegWrapper.getTokenBySymbolAsync(token2.symbol), tokenRegWrapper.getTokenBySymbolAsync(token1.symbol), @@ -193,52 +200,55 @@ describe('TokenRegistry', () => { it('should throw if the symbol already exists', async () => { await tokenRegWrapper.addTokenAsync(token2, owner); - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenSymbol.sendTransactionAsync(token1.address, token2.symbol, { from: owner, }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should throw if token does not exist', async () => { - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.setTokenSymbol.sendTransactionAsync(nullToken.address, token2.symbol, { from: owner, }), - ).to.be.rejectedWith(constants.REVERT); + ); }); }); describe('removeToken', () => { it('should throw if not called by owner', async () => { const index = new BigNumber(0); - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.removeToken.sendTransactionAsync(token1.address, index, { from: notOwner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should remove token metadata when called by owner', async () => { const index = new BigNumber(0); - await tokenReg.removeToken.sendTransactionAsync(token1.address, index, { - from: owner, - }); + await web3Wrapper.awaitTransactionSuccessAsync( + await tokenReg.removeToken.sendTransactionAsync(token1.address, index, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); const tokenData = await tokenRegWrapper.getTokenMetaDataAsync(token1.address); expect(tokenData).to.be.deep.equal(nullToken); }); it('should throw if token does not exist', async () => { const index = new BigNumber(0); - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.removeToken.sendTransactionAsync(nullToken.address, index, { from: owner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); it('should throw if token at given index does not match address', async () => { await tokenRegWrapper.addTokenAsync(token2, owner); const incorrectIndex = new BigNumber(0); - return expect( + return expectTransactionFailedWithoutReasonAsync( tokenReg.removeToken.sendTransactionAsync(token2.address, incorrectIndex, { from: owner }), - ).to.be.rejectedWith(constants.REVERT); + ); }); }); }); diff --git a/packages/contracts/test/token_transfer_proxy/auth.ts b/packages/contracts/test/token_transfer_proxy/auth.ts deleted file mode 100644 index a1ccc6ff7..000000000 --- a/packages/contracts/test/token_transfer_proxy/auth.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; -import { constants } from '../../util/constants'; -import { ContractName } from '../../util/types'; -import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('TokenTransferProxy', () => { - let owner: string; - let notOwner: string; - let address: string; - let tokenTransferProxy: TokenTransferProxyContract; - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - owner = address = accounts[0]; - notOwner = accounts[1]; - const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); - tokenTransferProxy = new TokenTransferProxyContract( - web3Wrapper, - tokenTransferProxyInstance.abi, - tokenTransferProxyInstance.address, - ); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('addAuthorizedAddress', () => { - it('should throw if not called by owner', async () => { - return expect( - tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(notOwner, { from: notOwner }), - ).to.be.rejectedWith(constants.REVERT); - }); - it('should allow owner to add an authorized address', async () => { - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }); - const isAuthorized = await tokenTransferProxy.authorized.callAsync(address); - expect(isAuthorized).to.be.true(); - }); - it('should throw if owner attempts to authorize a duplicate address', async () => { - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }); - return expect( - tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }), - ).to.be.rejectedWith(constants.REVERT); - }); - }); - - describe('removeAuthorizedAddress', () => { - it('should throw if not called by owner', async () => { - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }); - return expect( - tokenTransferProxy.removeAuthorizedAddress.sendTransactionAsync(address, { - from: notOwner, - }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should allow owner to remove an authorized address', async () => { - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }); - await tokenTransferProxy.removeAuthorizedAddress.sendTransactionAsync(address, { - from: owner, - }); - const isAuthorized = await tokenTransferProxy.authorized.callAsync(address); - expect(isAuthorized).to.be.false(); - }); - - it('should throw if owner attempts to remove an address that is not authorized', async () => { - return expect( - tokenTransferProxy.removeAuthorizedAddress.sendTransactionAsync(address, { - from: owner, - }), - ).to.be.rejectedWith(constants.REVERT); - }); - }); - - describe('getAuthorizedAddresses', () => { - it('should return all authorized addresses', async () => { - const initial = await tokenTransferProxy.getAuthorizedAddresses.callAsync(); - expect(initial).to.have.length(0); - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(address, { - from: owner, - }); - const afterAdd = await tokenTransferProxy.getAuthorizedAddresses.callAsync(); - expect(afterAdd).to.have.length(1); - expect(afterAdd).to.include(address); - - await tokenTransferProxy.removeAuthorizedAddress.sendTransactionAsync(address, { - from: owner, - }); - const afterRemove = await tokenTransferProxy.getAuthorizedAddresses.callAsync(); - expect(afterRemove).to.have.length(0); - }); - }); -}); diff --git a/packages/contracts/test/token_transfer_proxy/transfer_from.ts b/packages/contracts/test/token_transfer_proxy/transfer_from.ts deleted file mode 100644 index 415d068be..000000000 --- a/packages/contracts/test/token_transfer_proxy/transfer_from.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token'; -import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; -import { Balances } from '../../util/balances'; -import { constants } from '../../util/constants'; -import { ContractName } from '../../util/types'; -import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('TokenTransferProxy', () => { - let accounts: string[]; - let owner: string; - let notAuthorized: string; - const INIT_BAL = new BigNumber(100000000); - const INIT_ALLOW = new BigNumber(100000000); - - let tokenTransferProxy: TokenTransferProxyContract; - let rep: DummyTokenContract; - let dmyBalances: Balances; - - before(async () => { - accounts = await web3Wrapper.getAvailableAddressesAsync(); - owner = notAuthorized = accounts[0]; - const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); - tokenTransferProxy = new TokenTransferProxyContract( - web3Wrapper, - tokenTransferProxyInstance.abi, - tokenTransferProxyInstance.address, - ); - const repInstance = await deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS); - rep = new DummyTokenContract(web3Wrapper, repInstance.abi, repInstance.address); - - dmyBalances = new Balances([rep], [accounts[0], accounts[1]]); - await Promise.all([ - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { - from: accounts[0], - }), - rep.setBalance.sendTransactionAsync(accounts[0], INIT_BAL, { from: owner }), - rep.approve.sendTransactionAsync(tokenTransferProxy.address, INIT_ALLOW, { - from: accounts[1], - }), - rep.setBalance.sendTransactionAsync(accounts[1], INIT_BAL, { from: owner }), - ]); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - - describe('transferFrom', () => { - it('should throw when called by an unauthorized address', async () => { - expect( - tokenTransferProxy.transferFrom.sendTransactionAsync( - rep.address, - accounts[0], - accounts[1], - new BigNumber(1000), - { - from: notAuthorized, - }, - ), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should allow an authorized address to transfer', async () => { - const balances = await dmyBalances.getAsync(); - - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(notAuthorized, { - from: owner, - }); - const transferAmt = new BigNumber(10000); - await tokenTransferProxy.transferFrom.sendTransactionAsync( - rep.address, - accounts[0], - accounts[1], - transferAmt, - { - from: notAuthorized, - }, - ); - - const newBalances = await dmyBalances.getAsync(); - expect(newBalances[accounts[0]][rep.address]).to.be.bignumber.equal( - balances[accounts[0]][rep.address].minus(transferAmt), - ); - expect(newBalances[accounts[1]][rep.address]).to.be.bignumber.equal( - balances[accounts[1]][rep.address].add(transferAmt), - ); - }); - }); -}); diff --git a/packages/contracts/test/ether_token.ts b/packages/contracts/test/tokens/ether_token.ts index 6c7354c2b..a104fc915 100644 --- a/packages/contracts/test/ether_token.ts +++ b/packages/contracts/test/tokens/ether_token.ts @@ -1,15 +1,14 @@ -import { ZeroEx, ZeroExError } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber, promisify } from '@0xproject/utils'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as chai from 'chai'; -import { constants } from '../util/constants'; -import { ContractName } from '../util/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; +import { WETH9Contract } from '../../generated_contract_wrappers/weth9'; +import { artifacts } from '../utils/artifacts'; +import { expectInsufficientFundsAsync, expectTransactionFailedWithoutReasonAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; @@ -17,18 +16,22 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); describe('EtherToken', () => { let account: string; - const gasPrice = ZeroEx.toBaseUnitAmount(new BigNumber(20), 9); - let zeroEx: ZeroEx; - let etherTokenAddress: string; + const gasPrice = Web3Wrapper.toBaseUnitAmount(new BigNumber(20), 9); + let etherToken: WETH9Contract; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); account = accounts[0]; - const etherToken = await deployer.deployAsync(ContractName.EtherToken); - etherTokenAddress = etherToken.address; - zeroEx = new ZeroEx(web3.currentProvider, { + etherToken = await WETH9Contract.deployFrom0xArtifactAsync(artifacts.EtherToken, provider, { gasPrice, - networkId: constants.TESTRPC_NETWORK_ID, + ...txDefaults, }); }); beforeEach(async () => { @@ -42,23 +45,24 @@ describe('EtherToken', () => { const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); const ethToDeposit = initEthBalance.plus(1); - return expect(zeroEx.etherToken.depositAsync(etherTokenAddress, ethToDeposit, account)).to.be.rejectedWith( - ZeroExError.InsufficientEthBalanceForDeposit, - ); + return expectInsufficientFundsAsync(etherToken.deposit.sendTransactionAsync({ value: ethToDeposit })); }); it('should convert deposited Ether to wrapped Ether tokens', async () => { const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); - const initEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const initEthTokenBalance = await etherToken.balanceOf.callAsync(account); - const ethToDeposit = new BigNumber(web3.toWei(1, 'ether')); + const ethToDeposit = new BigNumber(Web3Wrapper.toWei(new BigNumber(1))); - const txHash = await zeroEx.etherToken.depositAsync(etherTokenAddress, ethToDeposit, account); - const receipt = await zeroEx.awaitTransactionMinedAsync(txHash); + const txHash = await etherToken.deposit.sendTransactionAsync({ value: ethToDeposit }); + const receipt = await web3Wrapper.awaitTransactionSuccessAsync( + txHash, + constants.AWAIT_TRANSACTION_MINED_MS, + ); const ethSpentOnGas = gasPrice.times(receipt.gasUsed); const finalEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); - const finalEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const finalEthTokenBalance = await etherToken.balanceOf.callAsync(account); expect(finalEthBalance).to.be.bignumber.equal(initEthBalance.minus(ethToDeposit.plus(ethSpentOnGas))); expect(finalEthTokenBalance).to.be.bignumber.equal(initEthTokenBalance.plus(ethToDeposit)); @@ -67,29 +71,35 @@ describe('EtherToken', () => { describe('withdraw', () => { it('should throw if caller attempts to withdraw greater than caller balance', async () => { - const initEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const initEthTokenBalance = await etherToken.balanceOf.callAsync(account); const ethTokensToWithdraw = initEthTokenBalance.plus(1); - return expect( - zeroEx.etherToken.withdrawAsync(etherTokenAddress, ethTokensToWithdraw, account), - ).to.be.rejectedWith(ZeroExError.InsufficientWEthBalanceForWithdrawal); + return expectTransactionFailedWithoutReasonAsync( + etherToken.withdraw.sendTransactionAsync(ethTokensToWithdraw), + ); }); it('should convert ether tokens to ether with sufficient balance', async () => { - const ethToDeposit = new BigNumber(web3.toWei(1, 'ether')); - await zeroEx.etherToken.depositAsync(etherTokenAddress, ethToDeposit, account); - const initEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const ethToDeposit = new BigNumber(Web3Wrapper.toWei(new BigNumber(1))); + await web3Wrapper.awaitTransactionSuccessAsync( + await etherToken.deposit.sendTransactionAsync({ value: ethToDeposit }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const initEthTokenBalance = await etherToken.balanceOf.callAsync(account); const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); const ethTokensToWithdraw = initEthTokenBalance; expect(ethTokensToWithdraw).to.not.be.bignumber.equal(0); - const txHash = await zeroEx.etherToken.withdrawAsync(etherTokenAddress, ethTokensToWithdraw, account, { - gasLimit: constants.MAX_ETHERTOKEN_WITHDRAW_GAS, + const txHash = await etherToken.withdraw.sendTransactionAsync(ethTokensToWithdraw, { + gas: constants.MAX_ETHERTOKEN_WITHDRAW_GAS, }); - const receipt = await zeroEx.awaitTransactionMinedAsync(txHash); + const receipt = await web3Wrapper.awaitTransactionSuccessAsync( + txHash, + constants.AWAIT_TRANSACTION_MINED_MS, + ); const ethSpentOnGas = gasPrice.times(receipt.gasUsed); const finalEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); - const finalEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const finalEthTokenBalance = await etherToken.balanceOf.callAsync(account); expect(finalEthBalance).to.be.bignumber.equal( initEthBalance.plus(ethTokensToWithdraw.minus(ethSpentOnGas)), @@ -101,22 +111,25 @@ describe('EtherToken', () => { describe('fallback', () => { it('should convert sent ether to ether tokens', async () => { const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); - const initEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const initEthTokenBalance = await etherToken.balanceOf.callAsync(account); - const ethToDeposit = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18); + const ethToDeposit = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18); const txHash = await web3Wrapper.sendTransactionAsync({ from: account, - to: etherTokenAddress, + to: etherToken.address, value: ethToDeposit, gasPrice, }); - const receipt = await zeroEx.awaitTransactionMinedAsync(txHash); + const receipt = await web3Wrapper.awaitTransactionSuccessAsync( + txHash, + constants.AWAIT_TRANSACTION_MINED_MS, + ); const ethSpentOnGas = gasPrice.times(receipt.gasUsed); const finalEthBalance = await web3Wrapper.getBalanceInWeiAsync(account); - const finalEthTokenBalance = await zeroEx.token.getBalanceAsync(etherTokenAddress, account); + const finalEthTokenBalance = await etherToken.balanceOf.callAsync(account); expect(finalEthBalance).to.be.bignumber.equal(initEthBalance.minus(ethToDeposit.plus(ethSpentOnGas))); expect(finalEthTokenBalance).to.be.bignumber.equal(initEthTokenBalance.plus(ethToDeposit)); diff --git a/packages/contracts/test/tokens/unlimited_allowance_token.ts b/packages/contracts/test/tokens/unlimited_allowance_token.ts new file mode 100644 index 000000000..81d931fc5 --- /dev/null +++ b/packages/contracts/test/tokens/unlimited_allowance_token.ts @@ -0,0 +1,191 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { RevertReason } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { artifacts } from '../utils/artifacts'; +import { expectContractCallFailed } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('UnlimitedAllowanceToken', () => { + let owner: string; + let spender: string; + const MAX_MINT_VALUE = new BigNumber(100000000000000000000); + let token: DummyERC20TokenContract; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owner = accounts[0]; + spender = accounts[1]; + token = await DummyERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyERC20Token, + provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + constants.DUMMY_TOKEN_DECIMALS, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.mint.sendTransactionAsync(MAX_MINT_VALUE, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('transfer', () => { + it('should throw if owner has insufficient balance', async () => { + const ownerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = ownerBalance.plus(1); + return expectContractCallFailed( + token.transfer.callAsync(spender, amountToTransfer, { from: owner }), + RevertReason.Erc20InsufficientBalance, + ); + }); + + it('should transfer balance from sender to receiver', async () => { + const receiver = spender; + const initOwnerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = new BigNumber(1); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.transfer.sendTransactionAsync(receiver, amountToTransfer, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const finalOwnerBalance = await token.balanceOf.callAsync(owner); + const finalReceiverBalance = await token.balanceOf.callAsync(receiver); + + const expectedFinalOwnerBalance = initOwnerBalance.minus(amountToTransfer); + const expectedFinalReceiverBalance = amountToTransfer; + expect(finalOwnerBalance).to.be.bignumber.equal(expectedFinalOwnerBalance); + expect(finalReceiverBalance).to.be.bignumber.equal(expectedFinalReceiverBalance); + }); + + it('should return true on a 0 value transfer', async () => { + const didReturnTrue = await token.transfer.callAsync(spender, new BigNumber(0), { + from: owner, + }); + expect(didReturnTrue).to.be.true(); + }); + }); + + describe('transferFrom', () => { + it('should throw if owner has insufficient balance', async () => { + const ownerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = ownerBalance.plus(1); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.approve.sendTransactionAsync(spender, amountToTransfer, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + return expectContractCallFailed( + token.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }), + RevertReason.Erc20InsufficientBalance, + ); + }); + + it('should throw if spender has insufficient allowance', async () => { + const ownerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = ownerBalance; + + const spenderAllowance = await token.allowance.callAsync(owner, spender); + const isSpenderAllowanceInsufficient = spenderAllowance.cmp(amountToTransfer) < 0; + expect(isSpenderAllowanceInsufficient).to.be.true(); + + return expectContractCallFailed( + token.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }), + RevertReason.Erc20InsufficientAllowance, + ); + }); + + it('should return true on a 0 value transfer', async () => { + const amountToTransfer = new BigNumber(0); + const didReturnTrue = await token.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }); + expect(didReturnTrue).to.be.true(); + }); + + it('should not modify spender allowance if spender allowance is 2^256 - 1', async () => { + const initOwnerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = initOwnerBalance; + const initSpenderAllowance = constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; + await web3Wrapper.awaitTransactionSuccessAsync( + await token.approve.sendTransactionAsync(spender, initSpenderAllowance, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newSpenderAllowance = await token.allowance.callAsync(owner, spender); + expect(initSpenderAllowance).to.be.bignumber.equal(newSpenderAllowance); + }); + + it('should transfer the correct balances if spender has sufficient allowance', async () => { + const initOwnerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = initOwnerBalance; + const initSpenderAllowance = initOwnerBalance; + await web3Wrapper.awaitTransactionSuccessAsync( + await token.approve.sendTransactionAsync(spender, initSpenderAllowance, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newOwnerBalance = await token.balanceOf.callAsync(owner); + const newSpenderBalance = await token.balanceOf.callAsync(spender); + + expect(newOwnerBalance).to.be.bignumber.equal(0); + expect(newSpenderBalance).to.be.bignumber.equal(initOwnerBalance); + }); + + it('should modify allowance if spender has sufficient allowance less than 2^256 - 1', async () => { + const initOwnerBalance = await token.balanceOf.callAsync(owner); + const amountToTransfer = initOwnerBalance; + const initSpenderAllowance = initOwnerBalance; + await web3Wrapper.awaitTransactionSuccessAsync( + await token.approve.sendTransactionAsync(spender, initSpenderAllowance, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await token.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newSpenderAllowance = await token.allowance.callAsync(owner, spender); + expect(newSpenderAllowance).to.be.bignumber.equal(0); + }); + }); +}); diff --git a/packages/contracts/test/tokens/zrx_token.ts b/packages/contracts/test/tokens/zrx_token.ts new file mode 100644 index 000000000..a0d77c924 --- /dev/null +++ b/packages/contracts/test/tokens/zrx_token.ts @@ -0,0 +1,206 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; + +import { ZRXTokenContract } from '../../generated_contract_wrappers/zrx_token'; +import { artifacts } from '../utils/artifacts'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('ZRXToken', () => { + let owner: string; + let spender: string; + let MAX_UINT: BigNumber; + let zrxToken: ZRXTokenContract; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owner = accounts[0]; + spender = accounts[1]; + zrxToken = await ZRXTokenContract.deployFrom0xArtifactAsync(artifacts.ZRX, provider, txDefaults); + MAX_UINT = constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('constants', () => { + it('should have 18 decimals', async () => { + const decimals = new BigNumber(await zrxToken.decimals.callAsync()); + const expectedDecimals = 18; + expect(decimals).to.be.bignumber.equal(expectedDecimals); + }); + + it('should have a total supply of 1 billion tokens', async () => { + const totalSupply = new BigNumber(await zrxToken.totalSupply.callAsync()); + const expectedTotalSupply = 1000000000; + expect(Web3Wrapper.toUnitAmount(totalSupply, 18)).to.be.bignumber.equal(expectedTotalSupply); + }); + + it('should be named 0x Protocol Token', async () => { + const name = await zrxToken.name.callAsync(); + const expectedName = '0x Protocol Token'; + expect(name).to.be.equal(expectedName); + }); + + it('should have the symbol ZRX', async () => { + const symbol = await zrxToken.symbol.callAsync(); + const expectedSymbol = 'ZRX'; + expect(symbol).to.be.equal(expectedSymbol); + }); + }); + + describe('constructor', () => { + it('should initialize owner balance to totalSupply', async () => { + const ownerBalance = await zrxToken.balanceOf.callAsync(owner); + const totalSupply = new BigNumber(await zrxToken.totalSupply.callAsync()); + expect(totalSupply).to.be.bignumber.equal(ownerBalance); + }); + }); + + describe('transfer', () => { + it('should transfer balance from sender to receiver', async () => { + const receiver = spender; + const initOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const amountToTransfer = new BigNumber(1); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transfer.sendTransactionAsync(receiver, amountToTransfer, { from: owner }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const finalOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const finalReceiverBalance = await zrxToken.balanceOf.callAsync(receiver); + + const expectedFinalOwnerBalance = initOwnerBalance.minus(amountToTransfer); + const expectedFinalReceiverBalance = amountToTransfer; + expect(finalOwnerBalance).to.be.bignumber.equal(expectedFinalOwnerBalance); + expect(finalReceiverBalance).to.be.bignumber.equal(expectedFinalReceiverBalance); + }); + + it('should return true on a 0 value transfer', async () => { + const didReturnTrue = await zrxToken.transfer.callAsync(spender, new BigNumber(0), { + from: owner, + }); + expect(didReturnTrue).to.be.true(); + }); + }); + + describe('transferFrom', () => { + it('should return false if owner has insufficient balance', async () => { + const ownerBalance = await zrxToken.balanceOf.callAsync(owner); + const amountToTransfer = ownerBalance.plus(1); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(spender, amountToTransfer, { + from: owner, + gas: constants.MAX_TOKEN_APPROVE_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const didReturnTrue = await zrxToken.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }); + expect(didReturnTrue).to.be.false(); + }); + + it('should return false if spender has insufficient allowance', async () => { + const ownerBalance = await zrxToken.balanceOf.callAsync(owner); + const amountToTransfer = ownerBalance; + + const spenderAllowance = await zrxToken.allowance.callAsync(owner, spender); + const isSpenderAllowanceInsufficient = spenderAllowance.cmp(amountToTransfer) < 0; + expect(isSpenderAllowanceInsufficient).to.be.true(); + + const didReturnTrue = await zrxToken.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }); + expect(didReturnTrue).to.be.false(); + }); + + it('should return true on a 0 value transfer', async () => { + const amountToTransfer = new BigNumber(0); + const didReturnTrue = await zrxToken.transferFrom.callAsync(owner, spender, amountToTransfer, { + from: spender, + }); + expect(didReturnTrue).to.be.true(); + }); + + it('should not modify spender allowance if spender allowance is 2^256 - 1', async () => { + const initOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const amountToTransfer = initOwnerBalance; + const initSpenderAllowance = MAX_UINT; + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(spender, initSpenderAllowance, { + from: owner, + gas: constants.MAX_TOKEN_APPROVE_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newSpenderAllowance = await zrxToken.allowance.callAsync(owner, spender); + expect(initSpenderAllowance).to.be.bignumber.equal(newSpenderAllowance); + }); + + it('should transfer the correct balances if spender has sufficient allowance', async () => { + const initOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const initSpenderBalance = await zrxToken.balanceOf.callAsync(spender); + const amountToTransfer = initOwnerBalance; + const initSpenderAllowance = initOwnerBalance; + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(spender, initSpenderAllowance), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const newSpenderBalance = await zrxToken.balanceOf.callAsync(spender); + + expect(newOwnerBalance).to.be.bignumber.equal(0); + expect(newSpenderBalance).to.be.bignumber.equal(initSpenderBalance.plus(initOwnerBalance)); + }); + + it('should modify allowance if spender has sufficient allowance less than 2^256 - 1', async () => { + const initOwnerBalance = await zrxToken.balanceOf.callAsync(owner); + const amountToTransfer = initOwnerBalance; + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.approve.sendTransactionAsync(spender, amountToTransfer), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await zrxToken.transferFrom.sendTransactionAsync(owner, spender, amountToTransfer, { + from: spender, + gas: constants.MAX_TOKEN_TRANSFERFROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const newSpenderAllowance = await zrxToken.allowance.callAsync(owner, spender); + expect(newSpenderAllowance).to.be.bignumber.equal(0); + }); + }); +}); diff --git a/packages/contracts/test/tutorials/arbitrage.ts b/packages/contracts/test/tutorials/arbitrage.ts index b2bbaf420..6851483cc 100644 --- a/packages/contracts/test/tutorials/arbitrage.ts +++ b/packages/contracts/test/tutorials/arbitrage.ts @@ -1,225 +1,260 @@ -import { ECSignature, SignedOrder, ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import ethUtil = require('ethereumjs-util'); -import * as Web3 from 'web3'; +// import { ECSignature, SignedOrder, ZeroEx } from '0x.js'; +// import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; +// import { ExchangeContractErrs } from 'ethereum-types'; +// import { BigNumber } from '@0xproject/utils'; +// import { Web3Wrapper } from '@0xproject/web3-wrapper'; +// import * as chai from 'chai'; +// import ethUtil = require('ethereumjs-util'); +// import * as Web3 from 'web3'; -import { ArbitrageContract } from '../../src/contract_wrappers/generated/arbitrage'; -import { EtherDeltaContract } from '../../src/contract_wrappers/generated/ether_delta'; -import { ExchangeContract } from '../../src/contract_wrappers/generated/exchange'; -import { Balances } from '../../util/balances'; -import { constants } from '../../util/constants'; -import { crypto } from '../../util/crypto'; -import { ExchangeWrapper } from '../../util/exchange_wrapper'; -import { OrderFactory } from '../../util/order_factory'; -import { BalancesByOwner, ContractName, ExchangeContractErrs } from '../../util/types'; -import { chaiSetup } from '../utils/chai_setup'; -import { deployer } from '../utils/deployer'; -import { web3, web3Wrapper } from '../utils/web3_wrapper'; +// import { AccountLevelsContract } from '../../src/generated_contract_wrappers/account_levels'; +// import { ArbitrageContract } from '../../src/generated_contract_wrappers/arbitrage'; +// import { DummyTokenContract } from '../../src/generated_contract_wrappers/dummy_token'; +// import { EtherDeltaContract } from '../../src/generated_contract_wrappers/ether_delta'; +// import { ExchangeContract } from '../../src/generated_contract_wrappers/exchange'; +// import { TokenTransferProxyContract } from '../../src/generated_contract_wrappers/token_transfer_proxy'; +// import { artifacts } from '../../util/artifacts'; +// import { Balances } from '../../util/balances'; +// import { constants } from '../../util/constants'; +// import { crypto } from '../../util/crypto'; +// import { ExchangeWrapper } from '../../util/exchange_wrapper'; +// import { OrderFactory } from '../../util/order_factory'; +// import { BalancesByOwner, ContractName } from '../../util/types'; +// import { chaiSetup } from '../utils/chai_setup'; -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +// import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; -describe('Arbitrage', () => { - let coinbase: string; - let maker: string; - let edMaker: string; - let edFrontRunner: string; - let amountGet: BigNumber; - let amountGive: BigNumber; - let makerTokenAmount: BigNumber; - let takerTokenAmount: BigNumber; - const feeRecipient = ZeroEx.NULL_ADDRESS; - const INITIAL_BALANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); - const INITIAL_ALLOWANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); +// chaiSetup.configure(); +// const expect = chai.expect; +// const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - let weth: Web3.ContractInstance; - let zrx: Web3.ContractInstance; - let arbitrage: ArbitrageContract; - let etherDelta: EtherDeltaContract; +// describe('Arbitrage', () => { +// let coinbase: string; +// let maker: string; +// let edMaker: string; +// let edFrontRunner: string; +// let amountGet: BigNumber; +// let amountGive: BigNumber; +// let makerTokenAmount: BigNumber; +// let takerTokenAmount: BigNumber; +// const feeRecipient = constants.NULL_ADDRESS; +// const INITIAL_BALANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); +// const INITIAL_ALLOWANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); - let signedOrder: SignedOrder; - let exWrapper: ExchangeWrapper; - let orderFactory: OrderFactory; +// let weth: DummyTokenContract; +// let zrx: DummyTokenContract; +// let arbitrage: ArbitrageContract; +// let etherDelta: EtherDeltaContract; - let zeroEx: ZeroEx; +// let signedOrder: SignedOrder; +// let exWrapper: ExchangeWrapper; +// let orderFactory: OrderFactory; - // From a bird's eye view - we create two orders. - // 0x order of 1 ZRX (maker) for 1 WETH (taker) - // ED order of 2 WETH (tokenGive) for 1 ZRX (tokenGet) - // And then we do an atomic arbitrage between them which gives us 1 WETH. - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - [coinbase, maker, edMaker, edFrontRunner] = accounts; - weth = await deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS); - zrx = await deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS); - const accountLevels = await deployer.deployAsync(ContractName.AccountLevels); - const edAdminAddress = accounts[0]; - const edMakerFee = 0; - const edTakerFee = 0; - const edFeeRebate = 0; - const etherDeltaInstance = await deployer.deployAsync(ContractName.EtherDelta, [ - edAdminAddress, - feeRecipient, - accountLevels.address, - edMakerFee, - edTakerFee, - edFeeRebate, - ]); - etherDelta = new EtherDeltaContract(web3Wrapper, etherDeltaInstance.abi, etherDeltaInstance.address); - const tokenTransferProxy = await deployer.deployAsync(ContractName.TokenTransferProxy); - const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ - zrx.address, - tokenTransferProxy.address, - ]); - await tokenTransferProxy.addAuthorizedAddress(exchangeInstance.address, { from: accounts[0] }); - zeroEx = new ZeroEx(web3.currentProvider, { - exchangeContractAddress: exchangeInstance.address, - networkId: constants.TESTRPC_NETWORK_ID, - }); - const exchange = new ExchangeContract(web3Wrapper, exchangeInstance.abi, exchangeInstance.address); - exWrapper = new ExchangeWrapper(exchange, zeroEx); +// let zeroEx: ZeroEx; - makerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18); - takerTokenAmount = makerTokenAmount; - const defaultOrderParams = { - exchangeContractAddress: exchange.address, - maker, - feeRecipient, - makerTokenAddress: zrx.address, - takerTokenAddress: weth.address, - makerTokenAmount, - takerTokenAmount, - makerFee: new BigNumber(0), - takerFee: new BigNumber(0), - }; - orderFactory = new OrderFactory(zeroEx, defaultOrderParams); - const arbitrageInstance = await deployer.deployAsync(ContractName.Arbitrage, [ - exchange.address, - etherDelta.address, - tokenTransferProxy.address, - ]); - arbitrage = new ArbitrageContract(web3Wrapper, arbitrageInstance.abi, arbitrageInstance.address); - // Enable arbitrage and withdrawals of tokens - await arbitrage.setAllowances.sendTransactionAsync(weth.address, { from: coinbase }); - await arbitrage.setAllowances.sendTransactionAsync(zrx.address, { from: coinbase }); +// // From a bird's eye view - we create two orders. +// // 0x order of 1 ZRX (maker) for 1 WETH (taker) +// // ED order of 2 WETH (tokenGive) for 1 ZRX (tokenGet) +// // And then we do an atomic arbitrage between them which gives us 1 WETH. +// before(async () => { +// const accounts = await web3Wrapper.getAvailableAddressesAsync(); +// [coinbase, maker, edMaker, edFrontRunner] = accounts; +// weth = await DummyTokenContract.deployFrom0xArtifactAsync( +// artifacts.DummyToken, +// provider, +// txDefaults, +// constants.DUMMY_TOKEN_NAME, +// constants.DUMMY_TOKEN_SYMBOL, +// constants.DUMMY_TOKEN_DECIMALS, +// constants.DUMMY_TOKEN_TOTAL_SUPPLY, +// ); +// zrx = await DummyTokenContract.deployFrom0xArtifactAsync( +// artifacts.DummyToken, +// provider, +// txDefaults, +// constants.DUMMY_TOKEN_NAME, +// constants.DUMMY_TOKEN_SYMBOL, +// constants.DUMMY_TOKEN_DECIMALS, +// constants.DUMMY_TOKEN_TOTAL_SUPPLY, +// ); +// const accountLevels = await AccountLevelsContract.deployFrom0xArtifactAsync( +// artifacts.AccountLevels, +// provider, +// txDefaults, +// ); +// const edAdminAddress = accounts[0]; +// const edMakerFee = new BigNumber(0); +// const edTakerFee = new BigNumber(0); +// const edFeeRebate = new BigNumber(0); +// etherDelta = await EtherDeltaContract.deployFrom0xArtifactAsync( +// artifacts.EtherDelta, +// provider, +// txDefaults, +// edAdminAddress, +// feeRecipient, +// accountLevels.address, +// edMakerFee, +// edTakerFee, +// edFeeRebate, +// ); +// const tokenTransferProxy = await TokenTransferProxyContract.deployFrom0xArtifactAsync( +// artifacts.TokenTransferProxy, +// provider, +// txDefaults, +// ); +// const exchange = await ExchangeContract.deployFrom0xArtifactAsync( +// artifacts.Exchange, +// provider, +// txDefaults, +// zrx.address, +// tokenTransferProxy.address, +// ); +// await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] }); +// zeroEx = new ZeroEx(provider, { +// exchangeContractAddress: exchange.address, +// networkId: constants.TESTRPC_NETWORK_ID, +// }); +// exWrapper = new ExchangeWrapper(exchange, provider); - // Give some tokens to arbitrage contract - await weth.setBalance(arbitrage.address, takerTokenAmount, { from: coinbase }); +// makerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18); +// takerTokenAmount = makerTokenAmount; +// const defaultOrderParams = { +// exchangeContractAddress: exchange.address, +// maker, +// feeRecipient, +// makerTokenAddress: zrx.address, +// takerTokenAddress: weth.address, +// makerTokenAmount, +// takerTokenAmount, +// makerFee: new BigNumber(0), +// takerFee: new BigNumber(0), +// }; +// orderFactory = new OrderFactory(zeroEx, defaultOrderParams); +// arbitrage = await ArbitrageContract.deployFrom0xArtifactAsync( +// artifacts.Arbitrage, +// provider, +// txDefaults, +// exchange.address, +// etherDelta.address, +// tokenTransferProxy.address, +// ); +// // Enable arbitrage and withdrawals of tokens +// await arbitrage.setAllowances.sendTransactionAsync(weth.address, { from: coinbase }); +// await arbitrage.setAllowances.sendTransactionAsync(zrx.address, { from: coinbase }); - // Fund the maker on exchange side - await zrx.setBalance(maker, makerTokenAmount, { from: coinbase }); - // Set the allowance for the maker on Exchange side - await zrx.approve(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: maker }); +// // Give some tokens to arbitrage contract +// await weth.setBalance.sendTransactionAsync(arbitrage.address, takerTokenAmount, { from: coinbase }); - amountGive = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18); - // Fund the maker on EtherDelta side - await weth.setBalance(edMaker, amountGive, { from: coinbase }); - // Set the allowance for the maker on EtherDelta side - await weth.approve(etherDelta.address, INITIAL_ALLOWANCE, { from: edMaker }); - // Deposit maker funds into EtherDelta - await etherDelta.depositToken.sendTransactionAsync(weth.address, amountGive, { from: edMaker }); +// // Fund the maker on exchange side +// await zrx.setBalance.sendTransactionAsync(maker, makerTokenAmount, { from: coinbase }); +// // Set the allowance for the maker on Exchange side +// await zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: maker }); - amountGet = makerTokenAmount; - // Fund the front runner on EtherDelta side - await zrx.setBalance(edFrontRunner, amountGet, { from: coinbase }); - // Set the allowance for the front-runner on EtherDelta side - await zrx.approve(etherDelta.address, INITIAL_ALLOWANCE, { from: edFrontRunner }); - // Deposit front runner funds into EtherDelta - await etherDelta.depositToken.sendTransactionAsync(zrx.address, amountGet, { from: edFrontRunner }); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('makeAtomicTrade', () => { - let addresses: string[]; - let values: BigNumber[]; - let v: number[]; - let r: string[]; - let s: string[]; - let tokenGet: string; - let tokenGive: string; - let expires: BigNumber; - let nonce: BigNumber; - let edSignature: ECSignature; - before(async () => { - signedOrder = await orderFactory.newSignedOrderAsync(); - tokenGet = zrx.address; - tokenGive = weth.address; - const blockNumber = await web3Wrapper.getBlockNumberAsync(); - const ED_ORDER_EXPIRATION_IN_BLOCKS = 10; - expires = new BigNumber(blockNumber + ED_ORDER_EXPIRATION_IN_BLOCKS); - nonce = new BigNumber(42); - const edOrderHash = `0x${crypto - .solSHA256([etherDelta.address, tokenGet, amountGet, tokenGive, amountGive, expires, nonce]) - .toString('hex')}`; - const shouldAddPersonalMessagePrefix = false; - edSignature = await zeroEx.signOrderHashAsync(edOrderHash, edMaker, shouldAddPersonalMessagePrefix); - addresses = [ - signedOrder.maker, - signedOrder.taker, - signedOrder.makerTokenAddress, - signedOrder.takerTokenAddress, - signedOrder.feeRecipient, - edMaker, - ]; - const fillTakerTokenAmount = takerTokenAmount; - const edFillAmount = makerTokenAmount; - values = [ - signedOrder.makerTokenAmount, - signedOrder.takerTokenAmount, - signedOrder.makerFee, - signedOrder.takerFee, - signedOrder.expirationUnixTimestampSec, - signedOrder.salt, - fillTakerTokenAmount, - amountGet, - amountGive, - expires, - nonce, - edFillAmount, - ]; - v = [signedOrder.ecSignature.v, edSignature.v]; - r = [signedOrder.ecSignature.r, edSignature.r]; - s = [signedOrder.ecSignature.s, edSignature.s]; - }); - it('should successfully execute the arbitrage if not front-runned', async () => { - const txHash = await arbitrage.makeAtomicTrade.sendTransactionAsync(addresses, values, v, r, s, { - from: coinbase, - }); - const res = await zeroEx.awaitTransactionMinedAsync(txHash); - const postBalance = await weth.balanceOf(arbitrage.address); - expect(postBalance).to.be.bignumber.equal(amountGive); - }); - it('should fail and revert if front-runned', async () => { - const preBalance = await weth.balanceOf(arbitrage.address); - // Front-running transaction - await etherDelta.trade.sendTransactionAsync( - tokenGet, - amountGet, - tokenGive, - amountGive, - expires, - nonce, - edMaker, - edSignature.v, - edSignature.r, - edSignature.s, - amountGet, - { from: edFrontRunner }, - ); - // tslint:disable-next-line:await-promise - await expect( - arbitrage.makeAtomicTrade.sendTransactionAsync(addresses, values, v, r, s, { from: coinbase }), - ).to.be.rejectedWith(constants.REVERT); - const postBalance = await weth.balanceOf(arbitrage.address); - expect(preBalance).to.be.bignumber.equal(postBalance); - }); - }); -}); +// amountGive = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18); +// // Fund the maker on EtherDelta side +// await weth.setBalance.sendTransactionAsync(edMaker, amountGive, { from: coinbase }); +// // Set the allowance for the maker on EtherDelta side +// await weth.approve.sendTransactionAsync(etherDelta.address, INITIAL_ALLOWANCE, { from: edMaker }); +// // Deposit maker funds into EtherDelta +// await etherDelta.depositToken.sendTransactionAsync(weth.address, amountGive, { from: edMaker }); + +// amountGet = makerTokenAmount; +// // Fund the front runner on EtherDelta side +// await zrx.setBalance.sendTransactionAsync(edFrontRunner, amountGet, { from: coinbase }); +// // Set the allowance for the front-runner on EtherDelta side +// await zrx.approve.sendTransactionAsync(etherDelta.address, INITIAL_ALLOWANCE, { from: edFrontRunner }); +// // Deposit front runner funds into EtherDelta +// await etherDelta.depositToken.sendTransactionAsync(zrx.address, amountGet, { from: edFrontRunner }); +// }); +// beforeEach(async () => { +// await blockchainLifecycle.startAsync(); +// }); +// afterEach(async () => { +// await blockchainLifecycle.revertAsync(); +// }); +// describe('makeAtomicTrade', () => { +// let addresses: string[]; +// let values: BigNumber[]; +// let v: number[]; +// let r: string[]; +// let s: string[]; +// let tokenGet: string; +// let tokenGive: string; +// let expires: BigNumber; +// let nonce: BigNumber; +// let edSignature: ECSignature; +// before(async () => { +// signedOrder = await orderFactory.newSignedOrderAsync(); +// tokenGet = zrx.address; +// tokenGive = weth.address; +// const blockNumber = await web3Wrapper.getBlockNumberAsync(); +// const ED_ORDER_EXPIRATION_IN_BLOCKS = 10; +// expires = new BigNumber(blockNumber + ED_ORDER_EXPIRATION_IN_BLOCKS); +// nonce = new BigNumber(42); +// const edOrderHash = `0x${crypto +// .solSHA256([etherDelta.address, tokenGet, amountGet, tokenGive, amountGive, expires, nonce]) +// .toString('hex')}`; +// const shouldAddPersonalMessagePrefix = false; +// edSignature = await zeroEx.signOrderHashAsync(edOrderHash, edMaker, shouldAddPersonalMessagePrefix); +// addresses = [ +// signedOrder.maker, +// signedOrder.taker, +// signedOrder.makerTokenAddress, +// signedOrder.takerTokenAddress, +// signedOrder.feeRecipient, +// edMaker, +// ]; +// const fillTakerTokenAmount = takerTokenAmount; +// const edFillAmount = makerTokenAmount; +// values = [ +// signedOrder.makerTokenAmount, +// signedOrder.takerTokenAmount, +// signedOrder.makerFee, +// signedOrder.takerFee, +// signedOrder.expirationUnixTimestampSec, +// signedOrder.salt, +// fillTakerTokenAmount, +// amountGet, +// amountGive, +// expires, +// nonce, +// edFillAmount, +// ]; +// v = [signedOrder.ecSignature.v, edSignature.v]; +// r = [signedOrder.ecSignature.r, edSignature.r]; +// s = [signedOrder.ecSignature.s, edSignature.s]; +// }); +// it('should successfully execute the arbitrage if not front-runned', async () => { +// const txHash = await arbitrage.makeAtomicTrade.sendTransactionAsync(addresses, values, v, r, s, { +// from: coinbase, +// }); +// const res = await zeroEx.awaitTransactionMinedAsync(txHash); +// const postBalance = await weth.balanceOf.callAsync(arbitrage.address); +// expect(postBalance).to.be.bignumber.equal(amountGive); +// }); +// it('should fail and revert if front-runned', async () => { +// const preBalance = await weth.balanceOf.callAsync(arbitrage.address); +// // Front-running transaction +// await etherDelta.trade.sendTransactionAsync( +// tokenGet, +// amountGet, +// tokenGive, +// amountGive, +// expires, +// nonce, +// edMaker, +// edSignature.v, +// edSignature.r, +// edSignature.s, +// amountGet, +// { from: edFrontRunner }, +// ); +// // tslint:disable-next-line:await-promise +// await expect( +// arbitrage.makeAtomicTrade.sendTransactionAsync(addresses, values, v, r, s, { from: coinbase }), +// ).to.be.rejectedWith(constants.REVERT); +// const postBalance = await weth.balanceOf.callAsync(arbitrage.address); +// expect(preBalance).to.be.bignumber.equal(postBalance); +// }); +// }); +// }); diff --git a/packages/contracts/test/unlimited_allowance_token.ts b/packages/contracts/test/unlimited_allowance_token.ts deleted file mode 100644 index c1c518adc..000000000 --- a/packages/contracts/test/unlimited_allowance_token.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import { DummyTokenContract } from '../src/contract_wrappers/generated/dummy_token'; -import { constants } from '../util/constants'; -import { ContractName } from '../util/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('UnlimitedAllowanceToken', () => { - let owner: string; - let spender: string; - const config = { - networkId: constants.TESTRPC_NETWORK_ID, - }; - const zeroEx = new ZeroEx(web3.currentProvider, config); - - const MAX_MINT_VALUE = new BigNumber(100000000000000000000); - let tokenAddress: string; - let token: DummyTokenContract; - - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - owner = accounts[0]; - spender = accounts[1]; - const tokenInstance = await deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS); - token = new DummyTokenContract(web3Wrapper, tokenInstance.abi, tokenInstance.address); - await token.mint.sendTransactionAsync(MAX_MINT_VALUE, { from: owner }); - tokenAddress = token.address; - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('transfer', () => { - it('should throw if owner has insufficient balance', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = ownerBalance.plus(1); - return expect(token.transfer.callAsync(spender, amountToTransfer, { from: owner })).to.be.rejectedWith( - constants.REVERT, - ); - }); - - it('should transfer balance from sender to receiver', async () => { - const receiver = spender; - const initOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = new BigNumber(1); - await zeroEx.token.transferAsync(tokenAddress, owner, receiver, amountToTransfer); - const finalOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const finalReceiverBalance = await zeroEx.token.getBalanceAsync(tokenAddress, receiver); - - const expectedFinalOwnerBalance = initOwnerBalance.minus(amountToTransfer); - const expectedFinalReceiverBalance = amountToTransfer; - expect(finalOwnerBalance).to.be.bignumber.equal(expectedFinalOwnerBalance); - expect(finalReceiverBalance).to.be.bignumber.equal(expectedFinalReceiverBalance); - }); - - it('should return true on a 0 value transfer', async () => { - const didReturnTrue = await token.transfer.callAsync(spender, new BigNumber(0), { - from: owner, - }); - expect(didReturnTrue).to.be.true(); - }); - }); - - describe('transferFrom', () => { - it('should throw if owner has insufficient balance', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = ownerBalance.plus(1); - await zeroEx.token.setAllowanceAsync(tokenAddress, owner, spender, amountToTransfer); - return expect( - token.transferFrom.callAsync(owner, spender, amountToTransfer, { - from: spender, - }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should throw if spender has insufficient allowance', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = ownerBalance; - - const spenderAllowance = await zeroEx.token.getAllowanceAsync(tokenAddress, owner, spender); - const spenderAllowanceIsInsufficient = spenderAllowance.cmp(amountToTransfer) < 0; - expect(spenderAllowanceIsInsufficient).to.be.true(); - - return expect( - token.transferFrom.callAsync(owner, spender, amountToTransfer, { - from: spender, - }), - ).to.be.rejectedWith(constants.REVERT); - }); - - it('should return true on a 0 value transfer', async () => { - const amountToTransfer = new BigNumber(0); - const didReturnTrue = await token.transferFrom.callAsync(owner, spender, amountToTransfer, { - from: spender, - }); - expect(didReturnTrue).to.be.true(); - }); - - it('should not modify spender allowance if spender allowance is 2^256 - 1', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = initOwnerBalance; - const initSpenderAllowance = zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; - await zeroEx.token.setAllowanceAsync(tokenAddress, owner, spender, initSpenderAllowance); - await zeroEx.token.transferFromAsync(tokenAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newSpenderAllowance = await zeroEx.token.getAllowanceAsync(tokenAddress, owner, spender); - expect(initSpenderAllowance).to.be.bignumber.equal(newSpenderAllowance); - }); - - it('should transfer the correct balances if spender has sufficient allowance', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = initOwnerBalance; - const initSpenderAllowance = initOwnerBalance; - await zeroEx.token.setAllowanceAsync(tokenAddress, owner, spender, initSpenderAllowance); - await zeroEx.token.transferFromAsync(tokenAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const newSpenderBalance = await zeroEx.token.getBalanceAsync(tokenAddress, spender); - - expect(newOwnerBalance).to.be.bignumber.equal(0); - expect(newSpenderBalance).to.be.bignumber.equal(initOwnerBalance); - }); - - it('should modify allowance if spender has sufficient allowance less than 2^256 - 1', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(tokenAddress, owner); - const amountToTransfer = initOwnerBalance; - const initSpenderAllowance = initOwnerBalance; - await zeroEx.token.setAllowanceAsync(tokenAddress, owner, spender, initSpenderAllowance); - await zeroEx.token.transferFromAsync(tokenAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newSpenderAllowance = await zeroEx.token.getAllowanceAsync(tokenAddress, owner, spender); - expect(newSpenderAllowance).to.be.bignumber.equal(0); - }); - }); -}); diff --git a/packages/contracts/test/utils/abstract_asset_wrapper.ts b/packages/contracts/test/utils/abstract_asset_wrapper.ts new file mode 100644 index 000000000..4b56a8502 --- /dev/null +++ b/packages/contracts/test/utils/abstract_asset_wrapper.ts @@ -0,0 +1,3 @@ +export abstract class AbstractAssetWrapper { + public abstract getProxyId(): string; +} diff --git a/packages/contracts/test/utils/address_utils.ts b/packages/contracts/test/utils/address_utils.ts new file mode 100644 index 000000000..a9fb6921a --- /dev/null +++ b/packages/contracts/test/utils/address_utils.ts @@ -0,0 +1,10 @@ +import { crypto, generatePseudoRandomSalt } from '@0xproject/order-utils'; + +export const addressUtils = { + generatePseudoRandomAddress(): string { + const randomBigNum = generatePseudoRandomSalt(); + const randomBuff = crypto.solSHA3([randomBigNum]); + const randomAddress = `0x${randomBuff.slice(0, 20).toString('hex')}`; + return randomAddress; + }, +}; diff --git a/packages/contracts/test/utils/artifacts.ts b/packages/contracts/test/utils/artifacts.ts new file mode 100644 index 000000000..e608ee174 --- /dev/null +++ b/packages/contracts/test/utils/artifacts.ts @@ -0,0 +1,57 @@ +import { ContractArtifact } from '@0xproject/sol-compiler'; + +import * as AssetProxyOwner from '../../artifacts/AssetProxyOwner.json'; +import * as DummyERC20Token from '../../artifacts/DummyERC20Token.json'; +import * as DummyERC721Receiver from '../../artifacts/DummyERC721Receiver.json'; +import * as DummyERC721Token from '../../artifacts/DummyERC721Token.json'; +import * as ERC20Proxy from '../../artifacts/ERC20Proxy.json'; +import * as ERC721Proxy from '../../artifacts/ERC721Proxy.json'; +import * as Exchange from '../../artifacts/Exchange.json'; +import * as ExchangeWrapper from '../../artifacts/ExchangeWrapper.json'; +import * as Forwarder from '../../artifacts/Forwarder.json'; +import * as IAssetProxy from '../../artifacts/IAssetProxy.json'; +import * as MixinAuthorizable from '../../artifacts/MixinAuthorizable.json'; +import * as MultiSigWallet from '../../artifacts/MultiSigWallet.json'; +import * as MultiSigWalletWithTimeLock from '../../artifacts/MultiSigWalletWithTimeLock.json'; +import * as TestAssetProxyDispatcher from '../../artifacts/TestAssetProxyDispatcher.json'; +import * as TestAssetProxyOwner from '../../artifacts/TestAssetProxyOwner.json'; +import * as TestConstants from '../../artifacts/TestConstants.json'; +import * as TestExchangeInternals from '../../artifacts/TestExchangeInternals.json'; +import * as TestLibBytes from '../../artifacts/TestLibBytes.json'; +import * as TestLibs from '../../artifacts/TestLibs.json'; +import * as TestSignatureValidator from '../../artifacts/TestSignatureValidator.json'; +import * as TokenRegistry from '../../artifacts/TokenRegistry.json'; +import * as Validator from '../../artifacts/Validator.json'; +import * as Wallet from '../../artifacts/Wallet.json'; +import * as EtherToken from '../../artifacts/WETH9.json'; +import * as Whitelist from '../../artifacts/Whitelist.json'; +import * as ZRX from '../../artifacts/ZRXToken.json'; + +export const artifacts = { + AssetProxyOwner: (AssetProxyOwner as any) as ContractArtifact, + DummyERC20Token: (DummyERC20Token as any) as ContractArtifact, + DummyERC721Receiver: (DummyERC721Receiver as any) as ContractArtifact, + DummyERC721Token: (DummyERC721Token as any) as ContractArtifact, + ERC20Proxy: (ERC20Proxy as any) as ContractArtifact, + ERC721Proxy: (ERC721Proxy as any) as ContractArtifact, + Exchange: (Exchange as any) as ContractArtifact, + ExchangeWrapper: (ExchangeWrapper as any) as ContractArtifact, + EtherToken: (EtherToken as any) as ContractArtifact, + Forwarder: (Forwarder as any) as ContractArtifact, + IAssetProxy: (IAssetProxy as any) as ContractArtifact, + MixinAuthorizable: (MixinAuthorizable as any) as ContractArtifact, + MultiSigWallet: (MultiSigWallet as any) as ContractArtifact, + MultiSigWalletWithTimeLock: (MultiSigWalletWithTimeLock as any) as ContractArtifact, + TestAssetProxyOwner: (TestAssetProxyOwner as any) as ContractArtifact, + TestAssetProxyDispatcher: (TestAssetProxyDispatcher as any) as ContractArtifact, + TestConstants: (TestConstants as any) as ContractArtifact, + TestLibBytes: (TestLibBytes as any) as ContractArtifact, + TestLibs: (TestLibs as any) as ContractArtifact, + TestExchangeInternals: (TestExchangeInternals as any) as ContractArtifact, + TestSignatureValidator: (TestSignatureValidator as any) as ContractArtifact, + Validator: (Validator as any) as ContractArtifact, + Wallet: (Wallet as any) as ContractArtifact, + TokenRegistry: (TokenRegistry as any) as ContractArtifact, + Whitelist: (Whitelist as any) as ContractArtifact, + ZRX: (ZRX as any) as ContractArtifact, +}; diff --git a/packages/contracts/test/utils/assertions.ts b/packages/contracts/test/utils/assertions.ts new file mode 100644 index 000000000..61df800c8 --- /dev/null +++ b/packages/contracts/test/utils/assertions.ts @@ -0,0 +1,186 @@ +import { RevertReason } from '@0xproject/types'; +import { logUtils } from '@0xproject/utils'; +import { NodeType } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import { TransactionReceipt, TransactionReceiptStatus, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { web3Wrapper } from './web3_wrapper'; + +const expect = chai.expect; + +let nodeType: NodeType | undefined; + +// Represents the return value of a `sendTransaction` call. The Promise should +// resolve with either a transaction receipt or a transaction hash. +export type sendTransactionResult = Promise<TransactionReceipt | TransactionReceiptWithDecodedLogs | string>; + +/** + * Returns ganacheError if the backing Ethereum node is Ganache and gethError + * if it is Geth. + * @param ganacheError the error to be returned if the backing node is Ganache. + * @param gethError the error to be returned if the backing node is Geth. + * @returns either the given ganacheError or gethError depending on the backing + * node. + */ +async function _getGanacheOrGethError(ganacheError: string, gethError: string): Promise<string> { + if (_.isUndefined(nodeType)) { + nodeType = await web3Wrapper.getNodeTypeAsync(); + } + switch (nodeType) { + case NodeType.Ganache: + return ganacheError; + case NodeType.Geth: + return gethError; + default: + throw new Error(`Unknown node type: ${nodeType}`); + } +} + +async function _getInsufficientFundsErrorMessageAsync(): Promise<string> { + return _getGanacheOrGethError("sender doesn't have enough funds", 'insufficient funds'); +} + +async function _getTransactionFailedErrorMessageAsync(): Promise<string> { + return _getGanacheOrGethError('revert', 'always failing transaction'); +} + +async function _getContractCallFailedErrorMessageAsync(): Promise<string> { + return _getGanacheOrGethError('revert', 'Contract call failed'); +} + +/** + * Returns the expected error message for an 'invalid opcode' resulting from a + * contract call. The exact error message depends on the backing Ethereum node. + */ +export async function getInvalidOpcodeErrorMessageForCallAsync(): Promise<string> { + return _getGanacheOrGethError('invalid opcode', 'Contract call failed'); +} + +/** + * Returns the expected error message for the given revert reason resulting from + * a sendTransaction call. The exact error message depends on the backing + * Ethereum node and whether it supports revert reasons. + * @param reason a specific revert reason. + * @returns the expected error message. + */ +export async function getRevertReasonOrErrorMessageForSendTransactionAsync(reason: RevertReason): Promise<string> { + return _getGanacheOrGethError(reason, 'always failing transaction'); +} + +/** + * Rejects if the given Promise does not reject with an error indicating + * insufficient funds. + * @param p a promise resulting from a contract call or sendTransaction call. + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectInsufficientFundsAsync<T>(p: Promise<T>): Promise<void> { + const errMessage = await _getInsufficientFundsErrorMessageAsync(); + return expect(p).to.be.rejectedWith(errMessage); +} + +/** + * Resolves if the the sendTransaction call fails with the given revert reason. + * However, since Geth does not support revert reasons for sendTransaction, this + * falls back to expectTransactionFailedWithoutReasonAsync if the backing + * Ethereum node is Geth. + * @param p a Promise resulting from a sendTransaction call + * @param reason a specific revert reason + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectTransactionFailedAsync(p: sendTransactionResult, reason: RevertReason): Promise<void> { + // HACK(albrow): This dummy `catch` should not be necessary, but if you + // remove it, there is an uncaught exception and the Node process will + // forcibly exit. It's possible this is a false positive in + // make-promises-safe. + p.catch(e => { + _.noop(e); + }); + + if (_.isUndefined(nodeType)) { + nodeType = await web3Wrapper.getNodeTypeAsync(); + } + switch (nodeType) { + case NodeType.Ganache: + return expect(p).to.be.rejectedWith(reason); + case NodeType.Geth: + logUtils.warn( + 'WARNING: Geth does not support revert reasons for sendTransaction. This test will pass if the transaction fails for any reason.', + ); + return expectTransactionFailedWithoutReasonAsync(p); + default: + throw new Error(`Unknown node type: ${nodeType}`); + } +} + +/** + * Resolves if the transaction fails without a revert reason, or if the + * corresponding transactionReceipt has a status of 0 or '0', indicating + * failure. + * @param p a Promise resulting from a sendTransaction call + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectTransactionFailedWithoutReasonAsync(p: sendTransactionResult): Promise<void> { + return p + .then(async result => { + let txReceiptStatus: TransactionReceiptStatus; + if (_.isString(result)) { + // Result is a txHash. We need to make a web3 call to get the + // receipt, then get the status from the receipt. + const txReceipt = await web3Wrapper.awaitTransactionMinedAsync(result); + txReceiptStatus = txReceipt.status; + } else if ('status' in result) { + // Result is a transaction receipt, so we can get the status + // directly. + txReceiptStatus = result.status; + } else { + throw new Error('Unexpected result type: ' + typeof result); + } + expect(_.toString(txReceiptStatus)).to.equal( + '0', + 'Expected transaction to fail but receipt had a non-zero status, indicating success', + ); + }) + .catch(async err => { + // If the promise rejects, we expect a specific error message, + // depending on the backing Ethereum node type. + const errMessage = await _getTransactionFailedErrorMessageAsync(); + expect(err.message).to.include(errMessage); + }); +} + +/** + * Resolves if the the contract call fails with the given revert reason. + * @param p a Promise resulting from a contract call + * @param reason a specific revert reason + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectContractCallFailed<T>(p: Promise<T>, reason: RevertReason): Promise<void> { + return expect(p).to.be.rejectedWith(reason); +} + +/** + * Resolves if the contract call fails without a revert reason. + * @param p a Promise resulting from a contract call + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectContractCallFailedWithoutReasonAsync<T>(p: Promise<T>): Promise<void> { + const errMessage = await _getContractCallFailedErrorMessageAsync(); + return expect(p).to.be.rejectedWith(errMessage); +} + +/** + * Resolves if the contract creation/deployment fails without a revert reason. + * @param p a Promise resulting from a contract creation/deployment + * @returns a new Promise which will reject if the conditions are not met and + * otherwise resolve with no value. + */ +export async function expectContractCreationFailedWithoutReason<T>(p: Promise<T>): Promise<void> { + const errMessage = await _getTransactionFailedErrorMessageAsync(); + return expect(p).to.be.rejectedWith(errMessage); +} diff --git a/packages/contracts/test/utils/asset_wrapper.ts b/packages/contracts/test/utils/asset_wrapper.ts new file mode 100644 index 000000000..e3a4800c6 --- /dev/null +++ b/packages/contracts/test/utils/asset_wrapper.ts @@ -0,0 +1,223 @@ +import { assetDataUtils } from '@0xproject/order-utils'; +import { AssetProxyId } from '@0xproject/types'; +import { BigNumber, errorUtils } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { AbstractAssetWrapper } from './abstract_asset_wrapper'; +import { constants } from './constants'; +import { ERC20Wrapper } from './erc20_wrapper'; +import { ERC721Wrapper } from './erc721_wrapper'; + +interface ProxyIdToAssetWrappers { + [proxyId: string]: AbstractAssetWrapper; +} + +/** + * This class abstracts away the differences between ERC20 and ERC721 tokens so that + * the logic that uses it does not need to care what standard a token belongs to. + */ +export class AssetWrapper { + private readonly _proxyIdToAssetWrappers: ProxyIdToAssetWrappers; + constructor(assetWrappers: AbstractAssetWrapper[]) { + this._proxyIdToAssetWrappers = {}; + _.each(assetWrappers, assetWrapper => { + const proxyId = assetWrapper.getProxyId(); + this._proxyIdToAssetWrappers[proxyId] = assetWrapper; + }); + } + public async getBalanceAsync(userAddress: string, assetData: string): Promise<BigNumber> { + const proxyId = assetDataUtils.decodeAssetProxyId(assetData); + switch (proxyId) { + case AssetProxyId.ERC20: { + const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper; + const balance = await erc20Wrapper.getBalanceAsync(userAddress, assetData); + return balance; + } + case AssetProxyId.ERC721: { + const assetWrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper; + const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData); + const isOwner = await assetWrapper.isOwnerAsync( + userAddress, + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + const balance = isOwner ? new BigNumber(1) : new BigNumber(0); + return balance; + } + default: + throw errorUtils.spawnSwitchErr('proxyId', proxyId); + } + } + public async setBalanceAsync(userAddress: string, assetData: string, desiredBalance: BigNumber): Promise<void> { + const proxyId = assetDataUtils.decodeAssetProxyId(assetData); + switch (proxyId) { + case AssetProxyId.ERC20: { + const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper; + await erc20Wrapper.setBalanceAsync(userAddress, assetData, desiredBalance); + return; + } + case AssetProxyId.ERC721: { + if (!desiredBalance.eq(0) && !desiredBalance.eq(1)) { + throw new Error(`Balance for ERC721 token can only be set to 0 or 1. Got: ${desiredBalance}`); + } + const erc721Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper; + const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData); + const doesTokenExist = erc721Wrapper.doesTokenExistAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + if (!doesTokenExist && desiredBalance.eq(1)) { + await erc721Wrapper.mintAsync(assetProxyData.tokenAddress, assetProxyData.tokenId, userAddress); + return; + } else if (!doesTokenExist && desiredBalance.eq(0)) { + return; // noop + } + const tokenOwner = await erc721Wrapper.ownerOfAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + if (userAddress !== tokenOwner && desiredBalance.eq(1)) { + await erc721Wrapper.transferFromAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + tokenOwner, + userAddress, + ); + } else if (tokenOwner === userAddress && desiredBalance.eq(0)) { + // Transfer token to someone else + const userAddresses = await (erc721Wrapper as any)._web3Wrapper.getAvailableAddressesAsync(); + const nonOwner = _.find(userAddresses, a => a !== userAddress); + await erc721Wrapper.transferFromAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + tokenOwner, + nonOwner, + ); + return; + } else if ( + (userAddress !== tokenOwner && desiredBalance.eq(0)) || + (tokenOwner === userAddress && desiredBalance.eq(1)) + ) { + return; // noop + } + break; + } + default: + throw errorUtils.spawnSwitchErr('proxyId', proxyId); + } + } + public async getProxyAllowanceAsync(userAddress: string, assetData: string): Promise<BigNumber> { + const proxyId = assetDataUtils.decodeAssetProxyId(assetData); + switch (proxyId) { + case AssetProxyId.ERC20: { + const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper; + const allowance = await erc20Wrapper.getProxyAllowanceAsync(userAddress, assetData); + return allowance; + } + case AssetProxyId.ERC721: { + const assetWrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper; + const erc721ProxyData = assetDataUtils.decodeERC721AssetData(assetData); + const isProxyApprovedForAll = await assetWrapper.isProxyApprovedForAllAsync( + userAddress, + erc721ProxyData.tokenAddress, + ); + if (isProxyApprovedForAll) { + return constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; + } + + const isProxyApproved = await assetWrapper.isProxyApprovedAsync( + erc721ProxyData.tokenAddress, + erc721ProxyData.tokenId, + ); + const allowance = isProxyApproved ? new BigNumber(1) : new BigNumber(0); + return allowance; + } + default: + throw errorUtils.spawnSwitchErr('proxyId', proxyId); + } + } + public async setProxyAllowanceAsync( + userAddress: string, + assetData: string, + desiredAllowance: BigNumber, + ): Promise<void> { + const proxyId = assetDataUtils.decodeAssetProxyId(assetData); + switch (proxyId) { + case AssetProxyId.ERC20: { + const erc20Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC20Wrapper; + await erc20Wrapper.setAllowanceAsync(userAddress, assetData, desiredAllowance); + return; + } + case AssetProxyId.ERC721: { + if ( + !desiredAllowance.eq(0) && + !desiredAllowance.eq(1) && + !desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS) + ) { + throw new Error( + `Allowance for ERC721 token can only be set to 0, 1 or 2^256-1. Got: ${desiredAllowance}`, + ); + } + const erc721Wrapper = this._proxyIdToAssetWrappers[proxyId] as ERC721Wrapper; + const assetProxyData = assetDataUtils.decodeERC721AssetData(assetData); + + const doesTokenExist = await erc721Wrapper.doesTokenExistAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + if (!doesTokenExist) { + throw new Error( + `Cannot setProxyAllowance on non-existent token: ${assetProxyData.tokenAddress} ${ + assetProxyData.tokenId + }`, + ); + } + const isProxyApprovedForAll = await erc721Wrapper.isProxyApprovedForAllAsync( + userAddress, + assetProxyData.tokenAddress, + ); + if (!isProxyApprovedForAll && desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { + const isApproved = true; + await erc721Wrapper.approveProxyForAllAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + isApproved, + ); + } else if (isProxyApprovedForAll && desiredAllowance.eq(0)) { + const isApproved = false; + await erc721Wrapper.approveProxyForAllAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + isApproved, + ); + } else if (isProxyApprovedForAll && desiredAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { + return; // Noop + } + + const isProxyApproved = await erc721Wrapper.isProxyApprovedAsync( + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + if (!isProxyApproved && desiredAllowance.eq(1)) { + await erc721Wrapper.approveProxyAsync(assetProxyData.tokenAddress, assetProxyData.tokenId); + } else if (isProxyApproved && desiredAllowance.eq(0)) { + // Remove approval + await erc721Wrapper.approveAsync( + constants.NULL_ADDRESS, + assetProxyData.tokenAddress, + assetProxyData.tokenId, + ); + } else if ( + (!isProxyApproved && desiredAllowance.eq(0)) || + (isProxyApproved && desiredAllowance.eq(1)) + ) { + return; // noop + } + + break; + } + default: + throw errorUtils.spawnSwitchErr('proxyId', proxyId); + } + } +} diff --git a/packages/contracts/test/utils/block_timestamp.ts b/packages/contracts/test/utils/block_timestamp.ts new file mode 100644 index 000000000..1159792c4 --- /dev/null +++ b/packages/contracts/test/utils/block_timestamp.ts @@ -0,0 +1,40 @@ +import * as _ from 'lodash'; + +import { constants } from './constants'; +import { web3Wrapper } from './web3_wrapper'; + +let firstAccount: string | undefined; + +/** + * Increases time by the given number of seconds and then mines a block so that + * the current block timestamp has the offset applied. + * @param seconds the number of seconds by which to incrase the time offset. + * @returns a new Promise which will resolve with the new total time offset or + * reject if the time could not be increased. + */ +export async function increaseTimeAndMineBlockAsync(seconds: number): Promise<number> { + if (_.isUndefined(firstAccount)) { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + firstAccount = accounts[0]; + } + + const offset = await web3Wrapper.increaseTimeAsync(seconds); + // Note: we need to send a transaction after increasing time so + // that a block is actually mined. The contract looks at the + // last mined block for the timestamp. + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ from: firstAccount, to: firstAccount, value: 0 }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + return offset; +} + +/** + * Returns the timestamp of the latest block in seconds since the Unix epoch. + * @returns a new Promise which will resolve with the timestamp in seconds. + */ +export async function getLatestBlockTimestampAsync(): Promise<number> { + const currentBlock = await web3Wrapper.getBlockAsync('latest'); + return currentBlock.timestamp; +} diff --git a/packages/contracts/test/utils/chai_setup.ts b/packages/contracts/test/utils/chai_setup.ts index 078edd309..1a8733093 100644 --- a/packages/contracts/test/utils/chai_setup.ts +++ b/packages/contracts/test/utils/chai_setup.ts @@ -4,7 +4,7 @@ import ChaiBigNumber = require('chai-bignumber'); import * as dirtyChai from 'dirty-chai'; export const chaiSetup = { - configure() { + configure(): void { chai.config.includeStack = true; chai.use(ChaiBigNumber()); chai.use(dirtyChai); diff --git a/packages/contracts/test/utils/combinatorial_utils.ts b/packages/contracts/test/utils/combinatorial_utils.ts new file mode 100644 index 000000000..d72b41f8a --- /dev/null +++ b/packages/contracts/test/utils/combinatorial_utils.ts @@ -0,0 +1,113 @@ +import { BigNumber } from '@0xproject/utils'; +import * as combinatorics from 'js-combinatorics'; + +import { testWithReferenceFuncAsync } from './test_with_reference'; + +// A set of values corresponding to the uint256 type in Solidity. This set +// contains some notable edge cases, including some values which will overflow +// the uint256 type when used in different mathematical operations. +export const uint256Values = [ + new BigNumber(0), + new BigNumber(1), + new BigNumber(2), + // Non-trivial big number. + new BigNumber(2).pow(64), + // Max that does not overflow when squared. + new BigNumber(2).pow(128).minus(1), + // Min that does overflow when squared. + new BigNumber(2).pow(128), + // Max that does not overflow when doubled. + new BigNumber(2).pow(255).minus(1), + // Min that does overflow when doubled. + new BigNumber(2).pow(255), + // Max that does not overflow. + new BigNumber(2).pow(256).minus(1), +]; + +// A set of values corresponding to the bytes32 type in Solidity. +export const bytes32Values = [ + // Min + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000002', + // Non-trivial big number. + '0x000000000000f000000000000000000000000000000000000000000000000000', + // Max + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', +]; + +export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, R>( + name: string, + referenceFunc: (p0: P0, p1: P1) => Promise<R>, + testFunc: (p0: P0, p1: P1) => Promise<R>, + allValues: [P0[], P1[]], +): Promise<void>; +export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, R>( + name: string, + referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>, + allValues: [P0[], P1[], P2[]], +): Promise<void>; +export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, P3, R>( + name: string, + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>, + allValues: [P0[], P1[], P2[], P3[]], +): Promise<void>; +export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, P3, P4, R>( + name: string, + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>, + allValues: [P0[], P1[], P2[], P3[], P4[]], +): Promise<void>; + +/** + * Uses combinatorics to test the behavior of a test function by comparing it to + * the expected behavior (defined by a reference function) for a large number of + * possible input values. + * + * First generates test cases by taking the cartesian product of the given + * values. Each test case is a set of N values corresponding to the N arguments + * for the test func and the reference func. For each test case, first the + * reference function will be called to obtain an "expected result", or if the + * reference function throws/rejects, an "expected error". Next, the test + * function will be called to obtain an "actual result", or if the test function + * throws/rejects, an "actual error". Each test case passes if at least one of + * the following conditions is met: + * + * 1) Neither the reference function or the test function throw and the + * "expected result" equals the "actual result". + * + * 2) Both the reference function and the test function throw and the "actual + * error" message *contains* the "expected error" message. + * + * The first test case which does not meet one of these conditions will cause + * the entire test to fail and this function will throw/reject. + * + * @param referenceFuncAsync a reference function implemented in pure + * JavaScript/TypeScript which accepts N arguments and returns the "expected + * result" or "expected error" for a given test case. + * @param testFuncAsync a test function which, e.g., makes a call or sends a + * transaction to a contract. It accepts the same N arguments returns the + * "actual result" or "actual error" for a given test case. + * @param values an array of N arrays. Each inner array is a set of possible + * values which are passed into both the reference function and the test + * function. + * @return A Promise that resolves if the test passes and rejects if the test + * fails, according to the rules described above. + */ +export async function testCombinatoriallyWithReferenceFuncAsync( + name: string, + referenceFuncAsync: (...args: any[]) => Promise<any>, + testFuncAsync: (...args: any[]) => Promise<any>, + allValues: any[], +): Promise<void> { + const testCases = combinatorics.cartesianProduct(...allValues); + let counter = 0; + testCases.forEach(async testCase => { + counter += 1; + it(`${name} ${counter}/${testCases.length}`, async () => { + await testWithReferenceFuncAsync(referenceFuncAsync, testFuncAsync, testCase as any); + }); + }); +} diff --git a/packages/contracts/test/utils/constants.ts b/packages/contracts/test/utils/constants.ts new file mode 100644 index 000000000..65eaee398 --- /dev/null +++ b/packages/contracts/test/utils/constants.ts @@ -0,0 +1,54 @@ +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +const TESTRPC_PRIVATE_KEYS_STRINGS = [ + '0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d', + '0x5d862464fe9303452126c8bc94274b8c5f9874cbd219789b3eb2128075a76f72', + '0xdf02719c4df8b9b8ac7f551fcb5d9ef48fa27eef7a66453879f4d8fdc6e78fb1', + '0xff12e391b79415e941a94de3bf3a9aee577aed0731e297d5cfa0b8a1e02fa1d0', + '0x752dd9cf65e68cfaba7d60225cbdbc1f4729dd5e5507def72815ed0d8abc6249', + '0xefb595a0178eb79a8df953f87c5148402a224cdf725e88c0146727c6aceadccd', + '0x83c6d2cc5ddcf9711a6d59b417dc20eb48afd58d45290099e5987e3d768f328f', + '0xbb2d3f7c9583780a7d3904a2f55d792707c345f21de1bacb2d389934d82796b2', + '0xb2fd4d29c1390b71b8795ae81196bfd60293adf99f9d32a0aff06288fcdac55f', + '0x23cb7121166b9a2f93ae0b7c05bde02eae50d64449b2cbb42bc84e9d38d6cc89', +]; + +export const constants = { + BASE_16: 16, + INVALID_OPCODE: 'invalid opcode', + TESTRPC_NETWORK_ID: 50, + // Note(albrow): In practice V8 and most other engines limit the minimum + // interval for setInterval to 10ms. We still set it to 0 here in order to + // ensure we always use the minimum interval. + AWAIT_TRANSACTION_MINED_MS: 0, + MAX_ETHERTOKEN_WITHDRAW_GAS: 43000, + MAX_EXECUTE_TRANSACTION_GAS: 1000000, + MAX_TOKEN_TRANSFERFROM_GAS: 80000, + MAX_TOKEN_APPROVE_GAS: 60000, + MAX_TRANSFER_FROM_GAS: 150000, + DUMMY_TOKEN_NAME: '', + DUMMY_TOKEN_SYMBOL: '', + DUMMY_TOKEN_DECIMALS: new BigNumber(18), + DUMMY_TOKEN_TOTAL_SUPPLY: new BigNumber(0), + NULL_BYTES: '0x', + NUM_DUMMY_ERC20_TO_DEPLOY: 3, + NUM_DUMMY_ERC721_TO_DEPLOY: 1, + NUM_ERC721_TOKENS_TO_MINT: 2, + NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), + TESTRPC_PRIVATE_KEYS: _.map(TESTRPC_PRIVATE_KEYS_STRINGS, privateKeyString => ethUtil.toBuffer(privateKeyString)), + INITIAL_ERC20_BALANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18), + INITIAL_ERC20_ALLOWANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18), + STATIC_ORDER_PARAMS: { + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18), + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + }, + WORD_LENGTH: 32, + ZERO_AMOUNT: new BigNumber(0), + PERCENTAGE_DENOMINATOR: new BigNumber(10).pow(18), +}; diff --git a/packages/contracts/test/utils/coverage.ts b/packages/contracts/test/utils/coverage.ts new file mode 100644 index 000000000..de29a3ecc --- /dev/null +++ b/packages/contracts/test/utils/coverage.ts @@ -0,0 +1,21 @@ +import { devConstants } from '@0xproject/dev-utils'; +import { CoverageSubprovider, SolCompilerArtifactAdapter } from '@0xproject/sol-cov'; +import * as _ from 'lodash'; + +let coverageSubprovider: CoverageSubprovider; + +export const coverage = { + getCoverageSubproviderSingleton(): CoverageSubprovider { + if (_.isUndefined(coverageSubprovider)) { + coverageSubprovider = coverage._getCoverageSubprovider(); + } + return coverageSubprovider; + }, + _getCoverageSubprovider(): CoverageSubprovider { + const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS; + const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter(); + const isVerbose = true; + const subprovider = new CoverageSubprovider(solCompilerArtifactAdapter, defaultFromAddress, isVerbose); + return subprovider; + }, +}; diff --git a/packages/contracts/test/utils/deployer.ts b/packages/contracts/test/utils/deployer.ts deleted file mode 100644 index 1baa47f09..000000000 --- a/packages/contracts/test/utils/deployer.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Deployer } from '@0xproject/deployer'; -import { devConstants } from '@0xproject/dev-utils'; -import * as path from 'path'; - -import { constants } from '../../util/constants'; - -import { web3 } from './web3_wrapper'; - -const deployerOpts = { - web3Provider: web3.currentProvider, - artifactsDir: path.resolve('src', 'artifacts'), - jsonrpcUrl: devConstants.RPC_URL, - networkId: constants.TESTRPC_NETWORK_ID, - defaults: { - gas: devConstants.GAS_ESTIMATE, - }, -}; - -export const deployer = new Deployer(deployerOpts); diff --git a/packages/contracts/test/utils/erc20_wrapper.ts b/packages/contracts/test/utils/erc20_wrapper.ts new file mode 100644 index 000000000..424aae579 --- /dev/null +++ b/packages/contracts/test/utils/erc20_wrapper.ts @@ -0,0 +1,175 @@ +import { assetDataUtils } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; +import { ERC20ProxyContract } from '../../generated_contract_wrappers/erc20_proxy'; + +import { artifacts } from './artifacts'; +import { constants } from './constants'; +import { ERC20BalancesByOwner } from './types'; +import { txDefaults } from './web3_wrapper'; + +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; + constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) { + this._dummyTokenContracts = []; + this._web3Wrapper = new Web3Wrapper(provider); + this._provider = provider; + this._tokenOwnerAddresses = tokenOwnerAddresses; + this._contractOwnerAddress = contractOwnerAddress; + } + public async deployDummyTokensAsync( + numberToDeploy: number, + decimals: BigNumber, + ): Promise<DummyERC20TokenContract[]> { + for (let i = 0; i < numberToDeploy; i++) { + this._dummyTokenContracts.push( + await DummyERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyERC20Token, + this._provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + decimals, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ), + ); + } + return this._dummyTokenContracts; + } + public async deployProxyAsync(): Promise<ERC20ProxyContract> { + this._proxyContract = await ERC20ProxyContract.deployFrom0xArtifactAsync( + artifacts.ERC20Proxy, + this._provider, + txDefaults, + ); + this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync(); + return this._proxyContract; + } + public getProxyId(): string { + this._validateProxyContractExistsOrThrow(); + return this._proxyIdIfExists as string; + } + public async setBalancesAndAllowancesAsync(): Promise<void> { + this._validateDummyTokenContractsExistOrThrow(); + this._validateProxyContractExistsOrThrow(); + for (const dummyTokenContract of this._dummyTokenContracts) { + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + await this._web3Wrapper.awaitTransactionSuccessAsync( + await dummyTokenContract.setBalance.sendTransactionAsync( + tokenOwnerAddress, + constants.INITIAL_ERC20_BALANCE, + { from: this._contractOwnerAddress }, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await dummyTokenContract.approve.sendTransactionAsync( + (this._proxyContract as ERC20ProxyContract).address, + constants.INITIAL_ERC20_ALLOWANCE, + { from: tokenOwnerAddress }, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + } + } + public async getBalanceAsync(userAddress: string, assetData: string): Promise<BigNumber> { + const tokenContract = this._getTokenContractFromAssetData(assetData); + const balance = new BigNumber(await tokenContract.balanceOf.callAsync(userAddress)); + return balance; + } + public async setBalanceAsync(userAddress: string, assetData: string, amount: BigNumber): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(assetData); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.setBalance.sendTransactionAsync(userAddress, amount, { + from: this._contractOwnerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async getProxyAllowanceAsync(userAddress: string, assetData: string): Promise<BigNumber> { + const tokenContract = this._getTokenContractFromAssetData(assetData); + const proxyAddress = (this._proxyContract as ERC20ProxyContract).address; + const allowance = new BigNumber(await tokenContract.allowance.callAsync(userAddress, proxyAddress)); + return allowance; + } + public async setAllowanceAsync(userAddress: string, assetData: string, amount: BigNumber): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(assetData); + const proxyAddress = (this._proxyContract as ERC20ProxyContract).address; + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.approve.sendTransactionAsync(proxyAddress, amount, { + from: userAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async getBalancesAsync(): Promise<ERC20BalancesByOwner> { + this._validateDummyTokenContractsExistOrThrow(); + const balancesByOwner: ERC20BalancesByOwner = {}; + const balances: BigNumber[] = []; + const balanceInfo: Array<{ tokenOwnerAddress: string; tokenAddress: string }> = []; + for (const dummyTokenContract of this._dummyTokenContracts) { + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + balances.push(await dummyTokenContract.balanceOf.callAsync(tokenOwnerAddress)); + balanceInfo.push({ + tokenOwnerAddress, + tokenAddress: dummyTokenContract.address, + }); + } + } + _.forEach(balances, (balance, balanceIndex) => { + const tokenAddress = balanceInfo[balanceIndex].tokenAddress; + const tokenOwnerAddress = balanceInfo[balanceIndex].tokenOwnerAddress; + if (_.isUndefined(balancesByOwner[tokenOwnerAddress])) { + balancesByOwner[tokenOwnerAddress] = {}; + } + const wrappedBalance = new BigNumber(balance); + balancesByOwner[tokenOwnerAddress][tokenAddress] = wrappedBalance; + }); + return balancesByOwner; + } + public addDummyTokenContract(dummy: DummyERC20TokenContract): void { + if (!_.isUndefined(this._dummyTokenContracts)) { + this._dummyTokenContracts.push(dummy); + } + } + public addTokenOwnerAddress(address: string): void { + this._tokenOwnerAddresses.push(address); + } + public getTokenOwnerAddresses(): string[] { + return this._tokenOwnerAddresses; + } + public getTokenAddresses(): string[] { + const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address); + return tokenAddresses; + } + private _getTokenContractFromAssetData(assetData: string): DummyERC20TokenContract { + const erc20ProxyData = assetDataUtils.decodeERC20AssetData(assetData); + const tokenAddress = erc20ProxyData.tokenAddress; + const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress); + if (_.isUndefined(tokenContractIfExists)) { + throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`); + } + return tokenContractIfExists; + } + private _validateDummyTokenContractsExistOrThrow(): void { + if (_.isUndefined(this._dummyTokenContracts)) { + throw new Error('Dummy ERC20 tokens not yet deployed, please call "deployDummyTokensAsync"'); + } + } + private _validateProxyContractExistsOrThrow(): void { + if (_.isUndefined(this._proxyContract)) { + throw new Error('ERC20 proxy contract not yet deployed, please call "deployProxyAsync"'); + } + } +} diff --git a/packages/contracts/test/utils/erc721_wrapper.ts b/packages/contracts/test/utils/erc721_wrapper.ts new file mode 100644 index 000000000..d023b4d02 --- /dev/null +++ b/packages/contracts/test/utils/erc721_wrapper.ts @@ -0,0 +1,236 @@ +import { generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; +import { ERC721ProxyContract } from '../../generated_contract_wrappers/erc721_proxy'; + +import { artifacts } from './artifacts'; +import { constants } from './constants'; +import { ERC721TokenIdsByOwner } from './types'; +import { txDefaults } from './web3_wrapper'; + +export class ERC721Wrapper { + private readonly _tokenOwnerAddresses: string[]; + private readonly _contractOwnerAddress: string; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _provider: Provider; + private readonly _dummyTokenContracts: DummyERC721TokenContract[]; + private _proxyContract?: ERC721ProxyContract; + private _proxyIdIfExists?: string; + private _initialTokenIdsByOwner: ERC721TokenIdsByOwner = {}; + constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) { + this._web3Wrapper = new Web3Wrapper(provider); + this._provider = provider; + this._dummyTokenContracts = []; + this._tokenOwnerAddresses = tokenOwnerAddresses; + this._contractOwnerAddress = contractOwnerAddress; + } + public async deployDummyTokensAsync(): Promise<DummyERC721TokenContract[]> { + for (let i = 0; i < constants.NUM_DUMMY_ERC721_TO_DEPLOY; i++) { + this._dummyTokenContracts.push( + await DummyERC721TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyERC721Token, + this._provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + ), + ); + } + return this._dummyTokenContracts; + } + public async deployProxyAsync(): Promise<ERC721ProxyContract> { + this._proxyContract = await ERC721ProxyContract.deployFrom0xArtifactAsync( + artifacts.ERC721Proxy, + this._provider, + txDefaults, + ); + this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync(); + return this._proxyContract; + } + public getProxyId(): string { + this._validateProxyContractExistsOrThrow(); + return this._proxyIdIfExists as string; + } + public async setBalancesAndAllowancesAsync(): Promise<void> { + this._validateDummyTokenContractsExistOrThrow(); + this._validateProxyContractExistsOrThrow(); + this._initialTokenIdsByOwner = {}; + for (const dummyTokenContract of this._dummyTokenContracts) { + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + for (let i = 0; i < constants.NUM_ERC721_TOKENS_TO_MINT; i++) { + const tokenId = generatePseudoRandomSalt(); + await this.mintAsync(dummyTokenContract.address, tokenId, tokenOwnerAddress); + if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress])) { + this._initialTokenIdsByOwner[tokenOwnerAddress] = { + [dummyTokenContract.address]: [], + }; + } + if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address])) { + this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address] = []; + } + this._initialTokenIdsByOwner[tokenOwnerAddress][dummyTokenContract.address].push(tokenId); + + await this.approveProxyAsync(dummyTokenContract.address, tokenId); + } + } + } + } + public async doesTokenExistAsync(tokenAddress: string, tokenId: BigNumber): Promise<boolean> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const doesExist = await tokenContract.exists.callAsync(tokenId); + return doesExist; + } + public async approveProxyAsync(tokenAddress: string, tokenId: BigNumber): Promise<void> { + const proxyAddress = (this._proxyContract as ERC721ProxyContract).address; + await this.approveAsync(proxyAddress, tokenAddress, tokenId); + } + public async approveProxyForAllAsync(tokenAddress: string, tokenId: BigNumber, isApproved: boolean): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const tokenOwner = await this.ownerOfAsync(tokenAddress, tokenId); + const proxyAddress = (this._proxyContract as ERC721ProxyContract).address; + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.setApprovalForAll.sendTransactionAsync(proxyAddress, isApproved, { + from: tokenOwner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async approveAsync(to: string, tokenAddress: string, tokenId: BigNumber): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const tokenOwner = await this.ownerOfAsync(tokenAddress, tokenId); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.approve.sendTransactionAsync(to, tokenId, { + from: tokenOwner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async transferFromAsync( + tokenAddress: string, + tokenId: BigNumber, + currentOwner: string, + userAddress: string, + ): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.transferFrom.sendTransactionAsync(currentOwner, userAddress, tokenId, { + from: currentOwner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async mintAsync(tokenAddress: string, tokenId: BigNumber, userAddress: string): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.mint.sendTransactionAsync(userAddress, tokenId, { + from: this._contractOwnerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async burnAsync(tokenAddress: string, tokenId: BigNumber, owner: string): Promise<void> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.burn.sendTransactionAsync(owner, tokenId, { + from: this._contractOwnerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async ownerOfAsync(tokenAddress: string, tokenId: BigNumber): Promise<string> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const owner = await tokenContract.ownerOf.callAsync(tokenId); + return owner; + } + public async isOwnerAsync(userAddress: string, tokenAddress: string, tokenId: BigNumber): Promise<boolean> { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const tokenOwner = await tokenContract.ownerOf.callAsync(tokenId); + const isOwner = tokenOwner === userAddress; + return isOwner; + } + public async isProxyApprovedForAllAsync(userAddress: string, tokenAddress: string): Promise<boolean> { + this._validateProxyContractExistsOrThrow(); + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const operator = (this._proxyContract as ERC721ProxyContract).address; + const didApproveAll = await tokenContract.isApprovedForAll.callAsync(userAddress, operator); + return didApproveAll; + } + public async isProxyApprovedAsync(tokenAddress: string, tokenId: BigNumber): Promise<boolean> { + this._validateProxyContractExistsOrThrow(); + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const approvedAddress = await tokenContract.getApproved.callAsync(tokenId); + const proxyAddress = (this._proxyContract as ERC721ProxyContract).address; + const isProxyAnApprovedOperator = approvedAddress === proxyAddress; + return isProxyAnApprovedOperator; + } + public async getBalancesAsync(): Promise<ERC721TokenIdsByOwner> { + this._validateDummyTokenContractsExistOrThrow(); + this._validateBalancesAndAllowancesSetOrThrow(); + const tokenIdsByOwner: ERC721TokenIdsByOwner = {}; + const tokenOwnerAddresses: string[] = []; + const tokenInfo: Array<{ tokenId: BigNumber; tokenAddress: string }> = []; + for (const dummyTokenContract of this._dummyTokenContracts) { + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + const initialTokenOwnerIds = this._initialTokenIdsByOwner[tokenOwnerAddress][ + dummyTokenContract.address + ]; + for (const tokenId of initialTokenOwnerIds) { + tokenOwnerAddresses.push(await dummyTokenContract.ownerOf.callAsync(tokenId)); + tokenInfo.push({ + tokenId, + tokenAddress: dummyTokenContract.address, + }); + } + } + } + _.forEach(tokenOwnerAddresses, (tokenOwnerAddress, ownerIndex) => { + const tokenAddress = tokenInfo[ownerIndex].tokenAddress; + const tokenId = tokenInfo[ownerIndex].tokenId; + if (_.isUndefined(tokenIdsByOwner[tokenOwnerAddress])) { + tokenIdsByOwner[tokenOwnerAddress] = { + [tokenAddress]: [], + }; + } + if (_.isUndefined(tokenIdsByOwner[tokenOwnerAddress][tokenAddress])) { + tokenIdsByOwner[tokenOwnerAddress][tokenAddress] = []; + } + tokenIdsByOwner[tokenOwnerAddress][tokenAddress].push(tokenId); + }); + return tokenIdsByOwner; + } + public getTokenOwnerAddresses(): string[] { + return this._tokenOwnerAddresses; + } + public getTokenAddresses(): string[] { + const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address); + return tokenAddresses; + } + private _getTokenContractFromAssetData(tokenAddress: string): DummyERC721TokenContract { + const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress); + if (_.isUndefined(tokenContractIfExists)) { + throw new Error(`Token: ${tokenAddress} was not deployed through ERC20Wrapper`); + } + return tokenContractIfExists; + } + private _validateDummyTokenContractsExistOrThrow(): void { + if (_.isUndefined(this._dummyTokenContracts)) { + throw new Error('Dummy ERC721 tokens not yet deployed, please call "deployDummyTokensAsync"'); + } + } + private _validateProxyContractExistsOrThrow(): void { + if (_.isUndefined(this._proxyContract)) { + throw new Error('ERC721 proxy contract not yet deployed, please call "deployProxyAsync"'); + } + } + private _validateBalancesAndAllowancesSetOrThrow(): void { + if (_.keys(this._initialTokenIdsByOwner).length === 0) { + throw new Error( + 'Dummy ERC721 balances and allowances not yet set, please call "setBalancesAndAllowancesAsync"', + ); + } + } +} diff --git a/packages/contracts/test/utils/exchange_wrapper.ts b/packages/contracts/test/utils/exchange_wrapper.ts new file mode 100644 index 000000000..d57592d6d --- /dev/null +++ b/packages/contracts/test/utils/exchange_wrapper.ts @@ -0,0 +1,269 @@ +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { ExchangeContract } from '../../generated_contract_wrappers/exchange'; + +import { formatters } from './formatters'; +import { LogDecoder } from './log_decoder'; +import { orderUtils } from './order_utils'; +import { FillResults, OrderInfo, SignedTransaction } from './types'; + +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, this._exchange.address); + } + public async fillOrderAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const txHash = await this._exchange.fillOrder.sendTransactionAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + { from }, + ); + const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return txReceipt; + } + public async cancelOrderAsync(signedOrder: SignedOrder, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createCancel(signedOrder); + const txHash = await this._exchange.cancelOrder.sendTransactionAsync(params.order, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async fillOrKillOrderAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const txHash = await this._exchange.fillOrKillOrder.sendTransactionAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async fillOrderNoThrowAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber; gas?: number } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const txHash = await this._exchange.fillOrderNoThrow.sendTransactionAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + { from, gas: opts.gas }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async batchFillOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[] } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const txHash = await this._exchange.batchFillOrders.sendTransactionAsync( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async batchFillOrKillOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[] } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const txHash = await this._exchange.batchFillOrKillOrders.sendTransactionAsync( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async batchFillOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[]; gas?: number } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const txHash = await this._exchange.batchFillOrdersNoThrow.sendTransactionAsync( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + { from, gas: opts.gas }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async marketSellOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmount: BigNumber }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount); + const txHash = await this._exchange.marketSellOrders.sendTransactionAsync( + params.orders, + params.takerAssetFillAmount, + params.signatures, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async marketSellOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmount: BigNumber; gas?: number }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount); + const txHash = await this._exchange.marketSellOrdersNoThrow.sendTransactionAsync( + params.orders, + params.takerAssetFillAmount, + params.signatures, + { from, gas: opts.gas }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async marketBuyOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { makerAssetFillAmount: BigNumber }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount); + const txHash = await this._exchange.marketBuyOrders.sendTransactionAsync( + params.orders, + params.makerAssetFillAmount, + params.signatures, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async marketBuyOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { makerAssetFillAmount: BigNumber; gas?: number }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount); + const txHash = await this._exchange.marketBuyOrdersNoThrow.sendTransactionAsync( + params.orders, + params.makerAssetFillAmount, + params.signatures, + { from, gas: opts.gas }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async batchCancelOrdersAsync( + orders: SignedOrder[], + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchCancel(orders); + const txHash = await this._exchange.batchCancelOrders.sendTransactionAsync(params.orders, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async cancelOrdersUpToAsync(salt: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._exchange.cancelOrdersUpTo.sendTransactionAsync(salt, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async registerAssetProxyAsync( + assetProxyAddress: string, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._exchange.registerAssetProxy.sendTransactionAsync(assetProxyAddress, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async executeTransactionAsync( + signedTx: SignedTransaction, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._exchange.executeTransaction.sendTransactionAsync( + signedTx.salt, + signedTx.signerAddress, + signedTx.data, + signedTx.signature, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async getTakerAssetFilledAmountAsync(orderHashHex: string): Promise<BigNumber> { + const filledAmount = new BigNumber(await this._exchange.filled.callAsync(orderHashHex)); + return filledAmount; + } + public async isCancelledAsync(orderHashHex: string): Promise<boolean> { + const isCancelled = await this._exchange.cancelled.callAsync(orderHashHex); + return isCancelled; + } + public async getOrderInfoAsync(signedOrder: SignedOrder): Promise<OrderInfo> { + const orderInfo = (await this._exchange.getOrderInfo.callAsync(signedOrder)) as OrderInfo; + return orderInfo; + } + public async getOrdersInfoAsync(signedOrders: SignedOrder[]): Promise<OrderInfo[]> { + const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[]; + return ordersInfo; + } + public async matchOrdersAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight); + const txHash = await this._exchange.matchOrders.sendTransactionAsync( + params.left, + params.right, + params.leftSignature, + params.rightSignature, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async getFillOrderResultsAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<FillResults> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const fillResults = await this._exchange.fillOrder.callAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + { from }, + ); + return fillResults; + } + public abiEncodeFillOrder(signedOrder: SignedOrder, opts: { takerAssetFillAmount?: BigNumber } = {}): string { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const data = this._exchange.fillOrder.getABIEncodedTransactionData( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + return data; + } +} diff --git a/packages/contracts/test/utils/fill_order_combinatorial_utils.ts b/packages/contracts/test/utils/fill_order_combinatorial_utils.ts new file mode 100644 index 000000000..284c4a2db --- /dev/null +++ b/packages/contracts/test/utils/fill_order_combinatorial_utils.ts @@ -0,0 +1,913 @@ +import { + assetDataUtils, + BalanceAndProxyAllowanceLazyStore, + ExchangeTransferSimulator, + orderHashUtils, + OrderStateUtils, + OrderValidationUtils, +} from '@0xproject/order-utils'; +import { AssetProxyId, RevertReason, SignatureType, SignedOrder } from '@0xproject/types'; +import { BigNumber, errorUtils, logUtils } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/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_contract_wrappers/exchange'; +import { TestLibsContract } from '../../generated_contract_wrappers/test_libs'; + +import { artifacts } from './artifacts'; +import { expectTransactionFailedAsync } from './assertions'; +import { AssetWrapper } from './asset_wrapper'; +import { chaiSetup } from './chai_setup'; +import { constants } from './constants'; +import { ERC20Wrapper } from './erc20_wrapper'; +import { ERC721Wrapper } from './erc721_wrapper'; +import { ExchangeWrapper } from './exchange_wrapper'; +import { OrderFactoryFromScenario } from './order_factory_from_scenario'; +import { orderUtils } from './order_utils'; +import { signingUtils } from './signing_utils'; +import { SimpleAssetBalanceAndProxyAllowanceFetcher } from './simple_asset_balance_and_proxy_allowance_fetcher'; +import { SimpleOrderFilledCancelledFetcher } from './simple_order_filled_cancelled_fetcher'; +import { + AllowanceAmountScenario, + AssetDataScenario, + BalanceAmountScenario, + ExpirationTimeSecondsScenario, + FeeRecipientAddressScenario, + FillScenario, + OrderAssetAmountScenario, + TakerAssetFillAmountScenario, + TakerScenario, + TraderStateScenario, +} from './types'; + +chaiSetup.configure(); +const expect = chai.expect; + +/** + * Instantiates a new instance of FillOrderCombinatorialUtils. Since this method has some + * required async setup, a factory method is required. + * @param web3Wrapper Web3Wrapper instance + * @param txDefaults Default Ethereum tx options + * @return FillOrderCombinatorialUtils instance + */ +export async function fillOrderCombinatorialUtilsFactoryAsync( + web3Wrapper: Web3Wrapper, + txDefaults: Partial<TxData>, +): Promise<FillOrderCombinatorialUtils> { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const userAddresses = _.slice(accounts, 0, 5); + const [ownerAddress, makerAddress, takerAddress] = userAddresses; + const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; + + const provider = web3Wrapper.getProvider(); + const erc20Wrapper = new ERC20Wrapper(provider, userAddresses, ownerAddress); + const erc721Wrapper = new ERC721Wrapper(provider, userAddresses, ownerAddress); + + const erc20EighteenDecimalTokenCount = 3; + const eighteenDecimals = new BigNumber(18); + const [ + erc20EighteenDecimalTokenA, + erc20EighteenDecimalTokenB, + zrxToken, + ] = await erc20Wrapper.deployDummyTokensAsync(erc20EighteenDecimalTokenCount, eighteenDecimals); + const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + + const erc20FiveDecimalTokenCount = 2; + const fiveDecimals = new BigNumber(5); + const [erc20FiveDecimalTokenA, erc20FiveDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync( + erc20FiveDecimalTokenCount, + fiveDecimals, + ); + const 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], + erc721Token, + erc721Balances, + exchangeContract.address, + ); + + const testLibsContract = await TestLibsContract.deployFrom0xArtifactAsync(artifacts.TestLibs, provider, txDefaults); + + const fillOrderCombinatorialUtils = new FillOrderCombinatorialUtils( + orderFactory, + ownerAddress, + makerAddress, + makerPrivateKey, + takerAddress, + zrxAssetData, + exchangeWrapper, + assetWrapper, + testLibsContract, + ); + return fillOrderCombinatorialUtils; +} + +export class FillOrderCombinatorialUtils { + public orderFactory: OrderFactoryFromScenario; + public ownerAddress: string; + public makerAddress: string; + public makerPrivateKey: Buffer; + public takerAddress: string; + public zrxAssetData: string; + public exchangeWrapper: ExchangeWrapper; + public assetWrapper: AssetWrapper; + public testLibsContract: TestLibsContract; + public static generateFillOrderCombinations(): FillScenario[] { + const takerScenarios = [ + TakerScenario.Unspecified, + // TakerScenario.CorrectlySpecified, + // TakerScenario.IncorrectlySpecified, + ]; + const feeRecipientScenarios = [ + FeeRecipientAddressScenario.EthUserAddress, + // FeeRecipientAddressScenario.BurnAddress, + ]; + const makerAssetAmountScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Zero, + // OrderAssetAmountScenario.Small, + ]; + const takerAssetAmountScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Zero, + // OrderAssetAmountScenario.Small, + ]; + const makerFeeScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Small, + // OrderAssetAmountScenario.Zero, + ]; + const takerFeeScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Small, + // OrderAssetAmountScenario.Zero, + ]; + const expirationTimeSecondsScenario = [ + ExpirationTimeSecondsScenario.InFuture, + ExpirationTimeSecondsScenario.InPast, + ]; + const makerAssetDataScenario = [ + AssetDataScenario.ERC20FiveDecimals, + AssetDataScenario.ERC20NonZRXEighteenDecimals, + AssetDataScenario.ERC721, + AssetDataScenario.ZRXFeeToken, + ]; + const takerAssetDataScenario = [ + AssetDataScenario.ERC20FiveDecimals, + AssetDataScenario.ERC20NonZRXEighteenDecimals, + AssetDataScenario.ERC721, + AssetDataScenario.ZRXFeeToken, + ]; + const takerAssetFillAmountScenario = [ + TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, + // TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount, + // TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount, + ]; + const makerAssetBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const makerAssetAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const makerZRXBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const makerZRXAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const takerAssetBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const takerAssetAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const takerZRXBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const takerZRXAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const fillScenarioArrays = FillOrderCombinatorialUtils._getAllCombinations([ + takerScenarios, + feeRecipientScenarios, + makerAssetAmountScenario, + takerAssetAmountScenario, + makerFeeScenario, + takerFeeScenario, + expirationTimeSecondsScenario, + makerAssetDataScenario, + takerAssetDataScenario, + takerAssetFillAmountScenario, + makerAssetBalanceScenario, + makerAssetAllowanceScenario, + makerZRXBalanceScenario, + makerZRXAllowanceScenario, + takerAssetBalanceScenario, + takerAssetAllowanceScenario, + takerZRXBalanceScenario, + takerZRXAllowanceScenario, + ]); + + const fillScenarios = _.map(fillScenarioArrays, fillScenarioArray => { + // tslint:disable:custom-no-magic-numbers + const fillScenario: FillScenario = { + orderScenario: { + takerScenario: fillScenarioArray[0] as TakerScenario, + feeRecipientScenario: fillScenarioArray[1] as FeeRecipientAddressScenario, + makerAssetAmountScenario: fillScenarioArray[2] as OrderAssetAmountScenario, + takerAssetAmountScenario: fillScenarioArray[3] as OrderAssetAmountScenario, + makerFeeScenario: fillScenarioArray[4] as OrderAssetAmountScenario, + takerFeeScenario: fillScenarioArray[5] as OrderAssetAmountScenario, + expirationTimeSecondsScenario: fillScenarioArray[6] as ExpirationTimeSecondsScenario, + makerAssetDataScenario: fillScenarioArray[7] as AssetDataScenario, + takerAssetDataScenario: fillScenarioArray[8] as AssetDataScenario, + }, + takerAssetFillAmountScenario: fillScenarioArray[9] as TakerAssetFillAmountScenario, + makerStateScenario: { + traderAssetBalance: fillScenarioArray[10] as BalanceAmountScenario, + traderAssetAllowance: fillScenarioArray[11] as AllowanceAmountScenario, + zrxFeeBalance: fillScenarioArray[12] as BalanceAmountScenario, + zrxFeeAllowance: fillScenarioArray[13] as AllowanceAmountScenario, + }, + takerStateScenario: { + traderAssetBalance: fillScenarioArray[14] as BalanceAmountScenario, + traderAssetAllowance: fillScenarioArray[15] as AllowanceAmountScenario, + zrxFeeBalance: fillScenarioArray[16] as BalanceAmountScenario, + zrxFeeAllowance: fillScenarioArray[17] as AllowanceAmountScenario, + }, + }; + // tslint:enable:custom-no-magic-numbers + return fillScenario; + }); + + return fillScenarios; + } + /** + * Recursive implementation of generating all combinations of the supplied + * string-containing arrays. + */ + private static _getAllCombinations(arrays: string[][]): string[][] { + // Base case + if (arrays.length === 1) { + const remainingValues = _.map(arrays[0], val => { + return [val]; + }); + return remainingValues; + } else { + const result = []; + const restOfArrays = arrays.slice(1); + const allCombinationsOfRemaining = FillOrderCombinatorialUtils._getAllCombinations(restOfArrays); // recur with the rest of array + // tslint:disable:prefer-for-of + for (let i = 0; i < allCombinationsOfRemaining.length; i++) { + for (let j = 0; j < arrays[0].length; j++) { + result.push([arrays[0][j], ...allCombinationsOfRemaining[i]]); + } + } + // tslint:enable:prefer-for-of + return result; + } + } + constructor( + orderFactory: OrderFactoryFromScenario, + ownerAddress: string, + makerAddress: string, + makerPrivateKey: Buffer, + takerAddress: string, + zrxAssetData: string, + exchangeWrapper: ExchangeWrapper, + assetWrapper: AssetWrapper, + testLibsContract: TestLibsContract, + ) { + this.orderFactory = orderFactory; + this.ownerAddress = ownerAddress; + this.makerAddress = makerAddress; + this.makerPrivateKey = makerPrivateKey; + this.takerAddress = takerAddress; + this.zrxAssetData = zrxAssetData; + this.exchangeWrapper = exchangeWrapper; + this.assetWrapper = assetWrapper; + this.testLibsContract = testLibsContract; + } + public async testFillOrderScenarioAsync( + provider: Provider, + fillScenario: FillScenario, + isVerbose: boolean = false, + ): Promise<void> { + // 1. Generate order + const order = this.orderFactory.generateOrder(fillScenario.orderScenario); + + // 2. Sign order + const orderHashBuff = orderHashUtils.getOrderHashBuffer(order); + const signature = signingUtils.signMessage(orderHashBuff, this.makerPrivateKey, SignatureType.EthSign); + const signedOrder = { + ...order, + signature: `0x${signature.toString('hex')}`, + }; + + const balanceAndProxyAllowanceFetcher = new SimpleAssetBalanceAndProxyAllowanceFetcher(this.assetWrapper); + const orderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher( + this.exchangeWrapper, + this.zrxAssetData, + ); + + // 3. Figure out fill amount + const takerAssetFillAmount = await this._getTakerAssetFillAmountAsync( + signedOrder, + fillScenario.takerAssetFillAmountScenario, + balanceAndProxyAllowanceFetcher, + orderFilledCancelledFetcher, + ); + + // 4. Permutate the maker and taker balance/allowance scenarios + await this._modifyTraderStateAsync( + fillScenario.makerStateScenario, + fillScenario.takerStateScenario, + signedOrder, + takerAssetFillAmount, + ); + + // 5. If I fill it by X, what are the resulting balances/allowances/filled amounts expected? + const orderValidationUtils = new OrderValidationUtils(orderFilledCancelledFetcher); + const lazyStore = new BalanceAndProxyAllowanceLazyStore(balanceAndProxyAllowanceFetcher); + const exchangeTransferSimulator = new ExchangeTransferSimulator(lazyStore); + + let fillRevertReasonIfExists; + try { + await orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTransferSimulator, + provider, + signedOrder, + takerAssetFillAmount, + this.takerAddress, + this.zrxAssetData, + ); + if (isVerbose) { + logUtils.log(`Expecting fillOrder to succeed.`); + } + } catch (err) { + fillRevertReasonIfExists = err.message; + if (isVerbose) { + logUtils.log(`Expecting fillOrder to fail with:`); + logUtils.log(err); + } + } + + // 6. Fill the order + await this._fillOrderAndAssertOutcomeAsync( + signedOrder, + takerAssetFillAmount, + lazyStore, + fillRevertReasonIfExists, + ); + + await this._abiEncodeFillOrderAndAssertOutcomeAsync(signedOrder, takerAssetFillAmount); + } + private async _fillOrderAndAssertOutcomeAsync( + signedOrder: SignedOrder, + takerAssetFillAmount: BigNumber, + lazyStore: BalanceAndProxyAllowanceLazyStore, + fillRevertReasonIfExists: RevertReason | undefined, + ): Promise<void> { + if (!_.isUndefined(fillRevertReasonIfExists)) { + return expectTransactionFailedAsync( + this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { takerAssetFillAmount }), + fillRevertReasonIfExists, + ); + } + + const makerAddress = signedOrder.makerAddress; + const makerAssetData = signedOrder.makerAssetData; + const takerAssetData = signedOrder.takerAssetData; + const feeRecipient = signedOrder.feeRecipientAddress; + + const expMakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(makerAssetData, makerAddress); + const expMakerAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(makerAssetData, makerAddress); + const expTakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(takerAssetData, makerAddress); + const expZRXAssetBalanceOfMaker = await lazyStore.getBalanceAsync(this.zrxAssetData, makerAddress); + const expZRXAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(this.zrxAssetData, makerAddress); + const expTakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(takerAssetData, this.takerAddress); + const expTakerAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(takerAssetData, this.takerAddress); + const expMakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(makerAssetData, this.takerAddress); + const expZRXAssetBalanceOfTaker = await lazyStore.getBalanceAsync(this.zrxAssetData, this.takerAddress); + const expZRXAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync( + this.zrxAssetData, + this.takerAddress, + ); + const expZRXAssetBalanceOfFeeRecipient = await lazyStore.getBalanceAsync(this.zrxAssetData, feeRecipient); + + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + const alreadyFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash); + const remainingTakerAmountToFill = signedOrder.takerAssetAmount.minus(alreadyFilledTakerAmount); + const expFilledTakerAmount = takerAssetFillAmount.gt(remainingTakerAmountToFill) + ? remainingTakerAmountToFill + : alreadyFilledTakerAmount.add(takerAssetFillAmount); + + const expFilledMakerAmount = orderUtils.getPartialAmount( + expFilledTakerAmount, + signedOrder.takerAssetAmount, + signedOrder.makerAssetAmount, + ); + const expMakerFeePaid = orderUtils.getPartialAmount( + expFilledTakerAmount, + signedOrder.takerAssetAmount, + signedOrder.makerFee, + ); + const expTakerFeePaid = orderUtils.getPartialAmount( + 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'); + + expect(txReceipt.logs.length).to.be.equal(1, 'logs length'); + // tslint:disable-next-line:no-unnecessary-type-assertion + const log = txReceipt.logs[0] as LogWithDecodedArgs<ExchangeFillEventArgs>; + expect(log.args.makerAddress).to.be.equal(makerAddress, 'log.args.makerAddress'); + expect(log.args.takerAddress).to.be.equal(this.takerAddress, 'log.args.this.takerAddress'); + expect(log.args.feeRecipientAddress).to.be.equal(feeRecipient, 'log.args.feeRecipientAddress'); + expect(log.args.makerAssetFilledAmount).to.be.bignumber.equal( + expFilledMakerAmount, + 'log.args.makerAssetFilledAmount', + ); + expect(log.args.takerAssetFilledAmount).to.be.bignumber.equal( + expFilledTakerAmount, + 'log.args.takerAssetFilledAmount', + ); + expect(log.args.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'log.args.makerFeePaid'); + expect(log.args.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'logs.args.takerFeePaid'); + expect(log.args.orderHash).to.be.equal(orderHash, 'log.args.orderHash'); + expect(log.args.makerAssetData).to.be.equal(makerAssetData, 'log.args.makerAssetData'); + expect(log.args.takerAssetData).to.be.equal(takerAssetData, 'log.args.takerAssetData'); + + const actMakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, makerAssetData); + expect(actMakerAssetBalanceOfMaker).to.be.bignumber.equal( + expMakerAssetBalanceOfMaker, + 'makerAssetBalanceOfMaker', + ); + + const actMakerAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync( + makerAddress, + makerAssetData, + ); + expect(actMakerAssetAllowanceOfMaker).to.be.bignumber.equal( + expMakerAssetAllowanceOfMaker, + 'makerAssetAllowanceOfMaker', + ); + + const actTakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, takerAssetData); + expect(actTakerAssetBalanceOfMaker).to.be.bignumber.equal( + expTakerAssetBalanceOfMaker, + 'takerAssetBalanceOfMaker', + ); + + const actZRXAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, this.zrxAssetData); + expect(actZRXAssetBalanceOfMaker).to.be.bignumber.equal(expZRXAssetBalanceOfMaker, 'ZRXAssetBalanceOfMaker'); + + const actZRXAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync( + makerAddress, + this.zrxAssetData, + ); + expect(actZRXAssetAllowanceOfMaker).to.be.bignumber.equal( + expZRXAssetAllowanceOfMaker, + 'ZRXAssetAllowanceOfMaker', + ); + + const actTakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, takerAssetData); + expect(actTakerAssetBalanceOfTaker).to.be.bignumber.equal( + expTakerAssetBalanceOfTaker, + 'TakerAssetBalanceOfTaker', + ); + + const actTakerAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync( + this.takerAddress, + takerAssetData, + ); + + expect(actTakerAssetAllowanceOfTaker).to.be.bignumber.equal( + expTakerAssetAllowanceOfTaker, + 'TakerAssetAllowanceOfTaker', + ); + + const actMakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, makerAssetData); + expect(actMakerAssetBalanceOfTaker).to.be.bignumber.equal( + expMakerAssetBalanceOfTaker, + 'MakerAssetBalanceOfTaker', + ); + + const actZRXAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, this.zrxAssetData); + expect(actZRXAssetBalanceOfTaker).to.be.bignumber.equal(expZRXAssetBalanceOfTaker, 'ZRXAssetBalanceOfTaker'); + + const actZRXAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync( + this.takerAddress, + this.zrxAssetData, + ); + expect(actZRXAssetAllowanceOfTaker).to.be.bignumber.equal( + expZRXAssetAllowanceOfTaker, + 'ZRXAssetAllowanceOfTaker', + ); + + const actZRXAssetBalanceOfFeeRecipient = await this.assetWrapper.getBalanceAsync( + feeRecipient, + this.zrxAssetData, + ); + expect(actZRXAssetBalanceOfFeeRecipient).to.be.bignumber.equal( + expZRXAssetBalanceOfFeeRecipient, + 'ZRXAssetBalanceOfFeeRecipient', + ); + } + private async _abiEncodeFillOrderAndAssertOutcomeAsync( + signedOrder: SignedOrder, + takerAssetFillAmount: BigNumber, + ): Promise<void> { + const params = orderUtils.createFill(signedOrder, takerAssetFillAmount); + const expectedAbiEncodedData = this.exchangeWrapper.abiEncodeFillOrder(signedOrder, { takerAssetFillAmount }); + const libsAbiEncodedData = await this.testLibsContract.publicAbiEncodeFillOrder.callAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + expect(libsAbiEncodedData).to.be.equal(expectedAbiEncodedData, 'ABIEncodedFillOrderData'); + } + private async _getTakerAssetFillAmountAsync( + signedOrder: SignedOrder, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario, + balanceAndProxyAllowanceFetcher: SimpleAssetBalanceAndProxyAllowanceFetcher, + orderFilledCancelledFetcher: SimpleOrderFilledCancelledFetcher, + ): Promise<BigNumber> { + const orderStateUtils = new OrderStateUtils(balanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher); + const fillableTakerAssetAmount = await orderStateUtils.getMaxFillableTakerAssetAmountAsync( + signedOrder, + this.takerAddress, + ); + + let takerAssetFillAmount; + switch (takerAssetFillAmountScenario) { + case TakerAssetFillAmountScenario.Zero: + takerAssetFillAmount = new BigNumber(0); + break; + + case TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount: + takerAssetFillAmount = fillableTakerAssetAmount; + break; + + case TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount: + takerAssetFillAmount = fillableTakerAssetAmount.add(1); + break; + + case TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount: + const takerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.takerAssetData); + const makerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.makerAssetData); + const isEitherAssetERC721 = + takerAssetProxyId === AssetProxyId.ERC721 || makerAssetProxyId === AssetProxyId.ERC721; + if (isEitherAssetERC721) { + throw new Error( + 'Cannot test `TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount` together with ERC721 assets since orders involving ERC721 must always be filled exactly.', + ); + } + takerAssetFillAmount = fillableTakerAssetAmount.div(2).floor(); + break; + + default: + throw errorUtils.spawnSwitchErr('TakerAssetFillAmountScenario', takerAssetFillAmountScenario); + } + + return takerAssetFillAmount; + } + private async _modifyTraderStateAsync( + makerStateScenario: TraderStateScenario, + takerStateScenario: TraderStateScenario, + signedOrder: SignedOrder, + takerAssetFillAmount: BigNumber, + ): Promise<void> { + const makerAssetFillAmount = orderUtils.getPartialAmount( + 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.getPartialAmount( + 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.getPartialAmount( + 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/packages/contracts/test/utils/formatters.ts b/packages/contracts/test/utils/formatters.ts new file mode 100644 index 000000000..32e4787d6 --- /dev/null +++ b/packages/contracts/test/utils/formatters.ts @@ -0,0 +1,68 @@ +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { constants } from './constants'; +import { orderUtils } from './order_utils'; +import { BatchCancelOrders, BatchFillOrders, MarketBuyOrders, MarketSellOrders } from './types'; + +export const formatters = { + createBatchFill(signedOrders: SignedOrder[], takerAssetFillAmounts: BigNumber[] = []): BatchFillOrders { + const batchFill: BatchFillOrders = { + orders: [], + signatures: [], + takerAssetFillAmounts, + }; + _.forEach(signedOrders, signedOrder => { + const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + batchFill.orders.push(orderWithoutExchangeAddress); + batchFill.signatures.push(signedOrder.signature); + if (takerAssetFillAmounts.length < signedOrders.length) { + batchFill.takerAssetFillAmounts.push(signedOrder.takerAssetAmount); + } + }); + return batchFill; + }, + createMarketSellOrders(signedOrders: SignedOrder[], takerAssetFillAmount: BigNumber): MarketSellOrders { + const marketSellOrders: MarketSellOrders = { + orders: [], + signatures: [], + takerAssetFillAmount, + }; + _.forEach(signedOrders, (signedOrder, i) => { + const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + if (i !== 0) { + orderWithoutExchangeAddress.takerAssetData = constants.NULL_BYTES; + } + marketSellOrders.orders.push(orderWithoutExchangeAddress); + marketSellOrders.signatures.push(signedOrder.signature); + }); + return marketSellOrders; + }, + createMarketBuyOrders(signedOrders: SignedOrder[], makerAssetFillAmount: BigNumber): MarketBuyOrders { + const marketBuyOrders: MarketBuyOrders = { + orders: [], + signatures: [], + makerAssetFillAmount, + }; + _.forEach(signedOrders, (signedOrder, i) => { + const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + if (i !== 0) { + orderWithoutExchangeAddress.makerAssetData = constants.NULL_BYTES; + } + marketBuyOrders.orders.push(orderWithoutExchangeAddress); + marketBuyOrders.signatures.push(signedOrder.signature); + }); + return marketBuyOrders; + }, + createBatchCancel(signedOrders: SignedOrder[]): BatchCancelOrders { + const batchCancel: BatchCancelOrders = { + orders: [], + }; + _.forEach(signedOrders, signedOrder => { + const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder); + batchCancel.orders.push(orderWithoutExchangeAddress); + }); + return batchCancel; + }, +}; diff --git a/packages/contracts/test/utils/forwarder_wrapper.ts b/packages/contracts/test/utils/forwarder_wrapper.ts new file mode 100644 index 000000000..5b9a63ddf --- /dev/null +++ b/packages/contracts/test/utils/forwarder_wrapper.ts @@ -0,0 +1,118 @@ +import { SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs, TxDataPayable } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { ForwarderContract } from '../../generated_contract_wrappers/forwarder'; + +import { constants } from './constants'; +import { formatters } from './formatters'; +import { LogDecoder } from './log_decoder'; +import { MarketSellOrders } from './types'; + +export class ForwarderWrapper { + private readonly _web3Wrapper: Web3Wrapper; + private readonly _forwarderContract: ForwarderContract; + private readonly _logDecoder: LogDecoder; + public static getPercentageOfValue(value: BigNumber, percentage: number): BigNumber { + const numerator = constants.PERCENTAGE_DENOMINATOR.times(percentage).dividedToIntegerBy(100); + const newValue = value.times(numerator).dividedToIntegerBy(constants.PERCENTAGE_DENOMINATOR); + return newValue; + } + public static getWethForFeeOrders(feeAmount: BigNumber, feeOrders: SignedOrder[]): BigNumber { + let wethAmount = new BigNumber(0); + let remainingFeeAmount = feeAmount; + _.forEach(feeOrders, feeOrder => { + const feeAvailable = feeOrder.makerAssetAmount.minus(feeOrder.takerFee); + if (!remainingFeeAmount.isZero() && feeAvailable.gt(remainingFeeAmount)) { + wethAmount = wethAmount + .plus(feeOrder.takerAssetAmount.times(remainingFeeAmount).dividedToIntegerBy(feeAvailable)) + .plus(1); + remainingFeeAmount = new BigNumber(0); + } else if (!remainingFeeAmount.isZero()) { + wethAmount = wethAmount.plus(feeOrder.takerAssetAmount); + remainingFeeAmount = remainingFeeAmount.minus(feeAvailable); + } + }); + return wethAmount; + } + private static _createOptimizedOrders(signedOrders: SignedOrder[]): MarketSellOrders { + _.forEach(signedOrders, (signedOrder, index) => { + signedOrder.takerAssetData = constants.NULL_BYTES; + if (index > 0) { + signedOrder.makerAssetData = constants.NULL_BYTES; + } + }); + const params = formatters.createMarketSellOrders(signedOrders, constants.ZERO_AMOUNT); + return params; + } + private static _createOptimizedZrxOrders(signedOrders: SignedOrder[]): MarketSellOrders { + _.forEach(signedOrders, signedOrder => { + signedOrder.makerAssetData = constants.NULL_BYTES; + signedOrder.takerAssetData = constants.NULL_BYTES; + }); + const params = formatters.createMarketSellOrders(signedOrders, constants.ZERO_AMOUNT); + return params; + } + constructor(contractInstance: ForwarderContract, provider: Provider) { + this._forwarderContract = contractInstance; + this._web3Wrapper = new Web3Wrapper(provider); + this._logDecoder = new LogDecoder(this._web3Wrapper, this._forwarderContract.address); + } + public async marketSellOrdersWithEthAsync( + orders: SignedOrder[], + feeOrders: SignedOrder[], + txData: TxDataPayable, + opts: { feePercentage?: BigNumber; feeRecipient?: string } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = ForwarderWrapper._createOptimizedOrders(orders); + const feeParams = ForwarderWrapper._createOptimizedZrxOrders(feeOrders); + const feePercentage = _.isUndefined(opts.feePercentage) ? constants.ZERO_AMOUNT : opts.feePercentage; + const feeRecipient = _.isUndefined(opts.feeRecipient) ? constants.NULL_ADDRESS : opts.feeRecipient; + const txHash = await this._forwarderContract.marketSellOrdersWithEth.sendTransactionAsync( + params.orders, + params.signatures, + feeParams.orders, + feeParams.signatures, + feePercentage, + feeRecipient, + txData, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async marketBuyOrdersWithEthAsync( + orders: SignedOrder[], + feeOrders: SignedOrder[], + makerAssetFillAmount: BigNumber, + txData: TxDataPayable, + opts: { feePercentage?: BigNumber; feeRecipient?: string } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = ForwarderWrapper._createOptimizedOrders(orders); + const feeParams = ForwarderWrapper._createOptimizedZrxOrders(feeOrders); + const feePercentage = _.isUndefined(opts.feePercentage) ? constants.ZERO_AMOUNT : opts.feePercentage; + const feeRecipient = _.isUndefined(opts.feeRecipient) ? constants.NULL_ADDRESS : opts.feeRecipient; + const txHash = await this._forwarderContract.marketBuyOrdersWithEth.sendTransactionAsync( + params.orders, + makerAssetFillAmount, + params.signatures, + feeParams.orders, + feeParams.signatures, + feePercentage, + feeRecipient, + txData, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async withdrawAssetAsync( + assetData: string, + amount: BigNumber, + txData: TxDataPayable, + ): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._forwarderContract.withdrawAsset.sendTransactionAsync(assetData, amount, txData); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } +} diff --git a/packages/contracts/test/utils/log_decoder.ts b/packages/contracts/test/utils/log_decoder.ts new file mode 100644 index 000000000..5a4801319 --- /dev/null +++ b/packages/contracts/test/utils/log_decoder.ts @@ -0,0 +1,55 @@ +import { ContractArtifact } from '@0xproject/sol-compiler'; +import { AbiDecoder, BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { + AbiDefinition, + DecodedLogArgs, + LogEntry, + LogWithDecodedArgs, + RawLog, + TransactionReceiptWithDecodedLogs, +} from 'ethereum-types'; +import * as _ from 'lodash'; + +import { artifacts } from './artifacts'; +import { constants } from './constants'; + +export class LogDecoder { + private readonly _web3Wrapper: Web3Wrapper; + private readonly _contractAddress: string; + private readonly _abiDecoder: AbiDecoder; + public static wrapLogBigNumbers(log: any): any { + const argNames = _.keys(log.args); + for (const argName of argNames) { + const isWeb3BigNumber = _.startsWith(log.args[argName].constructor.toString(), 'function BigNumber('); + if (isWeb3BigNumber) { + log.args[argName] = new BigNumber(log.args[argName]); + } + } + } + constructor(web3Wrapper: Web3Wrapper, contractAddress: string) { + this._web3Wrapper = web3Wrapper; + this._contractAddress = contractAddress; + const abiArrays: AbiDefinition[][] = []; + _.forEach(artifacts, (artifact: ContractArtifact) => { + const compilerOutput = artifact.compilerOutput; + abiArrays.push(compilerOutput.abi); + }); + this._abiDecoder = new AbiDecoder(abiArrays); + } + public decodeLogOrThrow<ArgsType extends DecodedLogArgs>(log: LogEntry): LogWithDecodedArgs<ArgsType> | RawLog { + const logWithDecodedArgsOrLog = this._abiDecoder.tryToDecodeLogOrNoop(log); + // tslint:disable-next-line:no-unnecessary-type-assertion + if (_.isUndefined((logWithDecodedArgsOrLog as LogWithDecodedArgs<ArgsType>).args)) { + throw new Error(`Unable to decode log: ${JSON.stringify(log)}`); + } + LogDecoder.wrapLogBigNumbers(logWithDecodedArgsOrLog); + return logWithDecodedArgsOrLog; + } + public async getTxWithDecodedLogsAsync(txHash: string): Promise<TransactionReceiptWithDecodedLogs> { + const tx = await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); + tx.logs = _.filter(tx.logs, log => log.address === this._contractAddress); + tx.logs = _.map(tx.logs, log => this.decodeLogOrThrow(log)); + return tx; + } +} diff --git a/packages/contracts/test/utils/match_order_tester.ts b/packages/contracts/test/utils/match_order_tester.ts new file mode 100644 index 000000000..fa2eabc12 --- /dev/null +++ b/packages/contracts/test/utils/match_order_tester.ts @@ -0,0 +1,326 @@ +import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils'; +import { AssetProxyId, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { chaiSetup } from './chai_setup'; +import { ERC20Wrapper } from './erc20_wrapper'; +import { ERC721Wrapper } from './erc721_wrapper'; +import { ExchangeWrapper } from './exchange_wrapper'; +import { ERC20BalancesByOwner, ERC721TokenIdsByOwner, TransferAmountsByMatchOrders as TransferAmounts } from './types'; + +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 Compares a pair of ERC20 balances and a pair of ERC721 token owners. + /// @param expectedNewERC20BalancesByOwner Expected ERC20 balances. + /// @param realERC20BalancesByOwner Actual ERC20 balances. + /// @param expectedNewERC721TokenIdsByOwner Expected ERC721 token owners. + /// @param realERC721TokenIdsByOwner Actual ERC20 token owners. + /// @return True only if ERC20 balances match and ERC721 token owners match. + private static _compareExpectedAndRealBalances( + expectedNewERC20BalancesByOwner: ERC20BalancesByOwner, + realERC20BalancesByOwner: ERC20BalancesByOwner, + expectedNewERC721TokenIdsByOwner: ERC721TokenIdsByOwner, + realERC721TokenIdsByOwner: ERC721TokenIdsByOwner, + ): boolean { + // ERC20 Balances + const doesErc20BalancesMatch = _.isEqual(expectedNewERC20BalancesByOwner, realERC20BalancesByOwner); + if (!doesErc20BalancesMatch) { + return false; + } + // ERC721 Token Ids + const sortedExpectedNewERC721TokenIdsByOwner = _.mapValues( + expectedNewERC721TokenIdsByOwner, + tokenIdsByOwner => { + _.mapValues(tokenIdsByOwner, tokenIds => { + _.sortBy(tokenIds); + }); + }, + ); + const sortedNewERC721TokenIdsByOwner = _.mapValues(realERC721TokenIdsByOwner, tokenIdsByOwner => { + _.mapValues(tokenIdsByOwner, tokenIds => { + _.sortBy(tokenIds); + }); + }); + const doesErc721TokenIdsMatch = _.isEqual( + sortedExpectedNewERC721TokenIdsByOwner, + sortedNewERC721TokenIdsByOwner, + ); + return doesErc721TokenIdsMatch; + } + /// @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 validates results. + /// Validation either succeeds or throws. + /// @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 initialTakerAssetFilledAmountLeft Current amount the left order has been filled. + /// @param initialTakerAssetFilledAmountRight Current amount the right order has been filled. + /// @return New ERC20 balances & ERC721 token owners. + public async matchOrdersAndVerifyBalancesAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + takerAddress: string, + erc20BalancesByOwner: ERC20BalancesByOwner, + erc721TokenIdsByOwner: ERC721TokenIdsByOwner, + initialTakerAssetFilledAmountLeft?: BigNumber, + initialTakerAssetFilledAmountRight?: BigNumber, + ): Promise<[ERC20BalancesByOwner, ERC721TokenIdsByOwner]> { + // Verify Left order preconditions + const orderTakerAssetFilledAmountLeft = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderHashUtils.getOrderHashHex(signedOrderLeft), + ); + const expectedOrderFilledAmountLeft = initialTakerAssetFilledAmountLeft + ? initialTakerAssetFilledAmountLeft + : new BigNumber(0); + expect(expectedOrderFilledAmountLeft).to.be.bignumber.equal(orderTakerAssetFilledAmountLeft); + // Verify Right order preconditions + const orderTakerAssetFilledAmountRight = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderHashUtils.getOrderHashHex(signedOrderRight), + ); + const expectedOrderFilledAmountRight = initialTakerAssetFilledAmountRight + ? initialTakerAssetFilledAmountRight + : new BigNumber(0); + expect(expectedOrderFilledAmountRight).to.be.bignumber.equal(orderTakerAssetFilledAmountRight); + // Match left & right orders + await this._exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress); + const newERC20BalancesByOwner = await this._erc20Wrapper.getBalancesAsync(); + const newERC721TokenIdsByOwner = await this._erc721Wrapper.getBalancesAsync(); + // Calculate expected balance changes + const expectedTransferAmounts = await this._calculateExpectedTransferAmountsAsync( + signedOrderLeft, + signedOrderRight, + orderTakerAssetFilledAmountLeft, + orderTakerAssetFilledAmountRight, + ); + let expectedERC20BalancesByOwner: ERC20BalancesByOwner; + let expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [expectedERC20BalancesByOwner, expectedERC721TokenIdsByOwner] = this._calculateExpectedBalances( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + expectedTransferAmounts, + ); + // Assert our expected balances are equal to the actual balances + const didExpectedBalancesMatchRealBalances = MatchOrderTester._compareExpectedAndRealBalances( + expectedERC20BalancesByOwner, + newERC20BalancesByOwner, + expectedERC721TokenIdsByOwner, + newERC721TokenIdsByOwner, + ); + expect(didExpectedBalancesMatchRealBalances).to.be.true(); + return [newERC20BalancesByOwner, newERC721TokenIdsByOwner]; + } + /// @dev Calculates expected transfer amounts between order makers, fee recipients, and + /// the taker when two orders are matched. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param orderTakerAssetFilledAmountLeft How much left order has been filled, prior to matching orders. + /// @param orderTakerAssetFilledAmountRight How much the right order has been filled, prior to matching orders. + /// @return TransferAmounts A struct containing the expected transfer amounts. + private async _calculateExpectedTransferAmountsAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + orderTakerAssetFilledAmountLeft: BigNumber, + orderTakerAssetFilledAmountRight: BigNumber, + ): Promise<TransferAmounts> { + let amountBoughtByLeftMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderHashUtils.getOrderHashHex(signedOrderLeft), + ); + amountBoughtByLeftMaker = amountBoughtByLeftMaker.minus(orderTakerAssetFilledAmountLeft); + const amountSoldByLeftMaker = amountBoughtByLeftMaker + .times(signedOrderLeft.makerAssetAmount) + .dividedToIntegerBy(signedOrderLeft.takerAssetAmount); + const amountReceivedByRightMaker = amountBoughtByLeftMaker + .times(signedOrderRight.takerAssetAmount) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const amountReceivedByTaker = amountSoldByLeftMaker.minus(amountReceivedByRightMaker); + let amountBoughtByRightMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderHashUtils.getOrderHashHex(signedOrderRight), + ); + amountBoughtByRightMaker = amountBoughtByRightMaker.minus(orderTakerAssetFilledAmountRight); + const amountSoldByRightMaker = amountBoughtByRightMaker + .times(signedOrderRight.makerAssetAmount) + .dividedToIntegerBy(signedOrderRight.takerAssetAmount); + const amountReceivedByLeftMaker = amountSoldByRightMaker; + const feePaidByLeftMaker = signedOrderLeft.makerFee + .times(amountSoldByLeftMaker) + .dividedToIntegerBy(signedOrderLeft.makerAssetAmount); + const feePaidByRightMaker = signedOrderRight.makerFee + .times(amountSoldByRightMaker) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const feePaidByTakerLeft = signedOrderLeft.takerFee + .times(amountSoldByLeftMaker) + .dividedToIntegerBy(signedOrderLeft.makerAssetAmount); + const feePaidByTakerRight = signedOrderRight.takerFee + .times(amountSoldByRightMaker) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const totalFeePaidByTaker = feePaidByTakerLeft.add(feePaidByTakerRight); + const feeReceivedLeft = feePaidByLeftMaker.add(feePaidByTakerLeft); + const feeReceivedRight = feePaidByRightMaker.add(feePaidByTakerRight); + // Return values + const expectedTransferAmounts = { + // Left Maker + amountBoughtByLeftMaker, + amountSoldByLeftMaker, + amountReceivedByLeftMaker, + feePaidByLeftMaker, + // Right Maker + amountBoughtByRightMaker, + amountSoldByRightMaker, + amountReceivedByRightMaker, + feePaidByRightMaker, + // Taker + amountReceivedByTaker, + feePaidByTakerLeft, + feePaidByTakerRight, + totalFeePaidByTaker, + // Fee Recipients + feeReceivedLeft, + feeReceivedRight, + }; + return expectedTransferAmounts; + } + /// @dev Calculates the expected balances of order makers, fee recipients, and the taker, + /// as a result of matching two orders. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param takerAddress Address of taker (the address who matched the two orders) + /// @param erc20BalancesByOwner Current ERC20 balances. + /// @param erc721TokenIdsByOwner Current ERC721 token owners. + /// @param expectedTransferAmounts A struct containing the expected transfer amounts. + /// @return Expected ERC20 balances & ERC721 token owners after orders have been matched. + private _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.amountReceivedByRightMaker, + ); + // 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.amountReceivedByLeftMaker); + // 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.totalFeePaidByTaker); + // Left Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][ + this._feeTokenAddress + ] = expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][this._feeTokenAddress].add( + expectedTransferAmounts.feeReceivedLeft, + ); + // Right Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressRight][ + this._feeTokenAddress + ] = expectedNewERC20BalancesByOwner[feeRecipientAddressRight][this._feeTokenAddress].add( + expectedTransferAmounts.feeReceivedRight, + ); + + return [expectedNewERC20BalancesByOwner, expectedNewERC721TokenIdsByOwner]; + } +} diff --git a/packages/contracts/test/utils/multi_sig_wrapper.ts b/packages/contracts/test/utils/multi_sig_wrapper.ts new file mode 100644 index 000000000..8c8055d4a --- /dev/null +++ b/packages/contracts/test/utils/multi_sig_wrapper.ts @@ -0,0 +1,60 @@ +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { AssetProxyOwnerContract } from '../../generated_contract_wrappers/asset_proxy_owner'; +import { MultiSigWalletContract } from '../../generated_contract_wrappers/multi_sig_wallet'; + +import { constants } from './constants'; +import { LogDecoder } from './log_decoder'; + +export class MultiSigWrapper { + private readonly _multiSig: MultiSigWalletContract; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _logDecoder: LogDecoder; + constructor(multiSigContract: MultiSigWalletContract, provider: Provider) { + this._multiSig = multiSigContract; + this._web3Wrapper = new Web3Wrapper(provider); + this._logDecoder = new LogDecoder(this._web3Wrapper, this._multiSig.address); + } + public async submitTransactionAsync( + destination: string, + data: string, + from: string, + opts: { value?: BigNumber } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const value = _.isUndefined(opts.value) ? new BigNumber(0) : opts.value; + const txHash = await this._multiSig.submitTransaction.sendTransactionAsync(destination, value, data, { + from, + }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async confirmTransactionAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._multiSig.confirmTransaction.sendTransactionAsync(txId, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async executeTransactionAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._multiSig.executeTransaction.sendTransactionAsync(txId, { + from, + gas: constants.MAX_EXECUTE_TRANSACTION_GAS, + }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async executeRemoveAuthorizedAddressAtIndexAsync( + txId: BigNumber, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + // tslint:disable-next-line:no-unnecessary-type-assertion + const txHash = await (this + ._multiSig as AssetProxyOwnerContract).executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from, + gas: constants.MAX_EXECUTE_TRANSACTION_GAS, + }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } +} diff --git a/packages/contracts/test/utils/order_factory.ts b/packages/contracts/test/utils/order_factory.ts new file mode 100644 index 000000000..63a893695 --- /dev/null +++ b/packages/contracts/test/utils/order_factory.ts @@ -0,0 +1,38 @@ +import { generatePseudoRandomSalt, orderHashUtils } from '@0xproject/order-utils'; +import { Order, SignatureType, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +import { getLatestBlockTimestampAsync } from './block_timestamp'; +import { constants } from './constants'; +import { signingUtils } from './signing_utils'; + +export class OrderFactory { + private readonly _defaultOrderParams: Partial<Order>; + private readonly _privateKey: Buffer; + constructor(privateKey: Buffer, defaultOrderParams: Partial<Order>) { + this._defaultOrderParams = defaultOrderParams; + this._privateKey = privateKey; + } + public async newSignedOrderAsync( + customOrderParams: Partial<Order> = {}, + signatureType: SignatureType = SignatureType.EthSign, + ): Promise<SignedOrder> { + const tenMinutesInSeconds = 10 * 60; + const currentBlockTimestamp = await getLatestBlockTimestampAsync(); + const order = ({ + senderAddress: constants.NULL_ADDRESS, + expirationTimeSeconds: new BigNumber(currentBlockTimestamp).add(tenMinutesInSeconds), + salt: generatePseudoRandomSalt(), + takerAddress: constants.NULL_ADDRESS, + ...this._defaultOrderParams, + ...customOrderParams, + } as any) as Order; + const orderHashBuff = orderHashUtils.getOrderHashBuffer(order); + const signature = signingUtils.signMessage(orderHashBuff, this._privateKey, signatureType); + const signedOrder = { + ...order, + signature: `0x${signature.toString('hex')}`, + }; + return signedOrder; + } +} diff --git a/packages/contracts/test/utils/order_factory_from_scenario.ts b/packages/contracts/test/utils/order_factory_from_scenario.ts new file mode 100644 index 000000000..a908140b9 --- /dev/null +++ b/packages/contracts/test/utils/order_factory_from_scenario.ts @@ -0,0 +1,273 @@ +import { assetDataUtils, generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { Order } from '@0xproject/types'; +import { BigNumber, errorUtils } from '@0xproject/utils'; + +import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_erc721_token'; + +import { constants } from './constants'; +import { + AssetDataScenario, + ERC721TokenIdsByOwner, + ExpirationTimeSecondsScenario, + FeeRecipientAddressScenario, + OrderAssetAmountScenario, + OrderScenario, + TakerScenario, +} from './types'; + +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 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 _erc721Token: DummyERC721TokenContract; + private readonly _erc721Balances: ERC721TokenIdsByOwner; + private readonly _exchangeAddress: string; + constructor( + userAddresses: string[], + zrxAddress: string, + nonZrxERC20EighteenDecimalTokenAddresses: string[], + erc20FiveDecimalTokenAddresses: string[], + erc721Token: DummyERC721TokenContract, + erc721Balances: ERC721TokenIdsByOwner, + exchangeAddress: string, + ) { + this._userAddresses = userAddresses; + this._zrxAddress = zrxAddress; + this._nonZrxERC20EighteenDecimalTokenAddresses = nonZrxERC20EighteenDecimalTokenAddresses; + this._erc20FiveDecimalTokenAddresses = erc20FiveDecimalTokenAddresses; + 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; + 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; + 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; + 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; + 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; + 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; + 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/packages/contracts/test/utils/order_utils.ts b/packages/contracts/test/utils/order_utils.ts new file mode 100644 index 000000000..019f6e74b --- /dev/null +++ b/packages/contracts/test/utils/order_utils.ts @@ -0,0 +1,58 @@ +import { OrderWithoutExchangeAddress, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +import { constants } from './constants'; +import { CancelOrder, MatchOrder } from './types'; + +export const orderUtils = { + getPartialAmount(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { + const partialAmount = numerator + .mul(target) + .div(denominator) + .floor(); + return partialAmount; + }, + createFill: (signedOrder: SignedOrder, takerAssetFillAmount?: BigNumber) => { + const fill = { + order: orderUtils.getOrderWithoutExchangeAddress(signedOrder), + takerAssetFillAmount: takerAssetFillAmount || signedOrder.takerAssetAmount, + signature: signedOrder.signature, + }; + return fill; + }, + createCancel(signedOrder: SignedOrder, takerAssetCancelAmount?: BigNumber): CancelOrder { + const cancel = { + order: orderUtils.getOrderWithoutExchangeAddress(signedOrder), + takerAssetCancelAmount: takerAssetCancelAmount || signedOrder.takerAssetAmount, + }; + return cancel; + }, + getOrderWithoutExchangeAddress(signedOrder: SignedOrder): OrderWithoutExchangeAddress { + const orderStruct = { + senderAddress: signedOrder.senderAddress, + makerAddress: signedOrder.makerAddress, + takerAddress: signedOrder.takerAddress, + feeRecipientAddress: signedOrder.feeRecipientAddress, + makerAssetAmount: signedOrder.makerAssetAmount, + takerAssetAmount: signedOrder.takerAssetAmount, + makerFee: signedOrder.makerFee, + takerFee: signedOrder.takerFee, + expirationTimeSeconds: signedOrder.expirationTimeSeconds, + salt: signedOrder.salt, + makerAssetData: signedOrder.makerAssetData, + takerAssetData: signedOrder.takerAssetData, + }; + return orderStruct; + }, + createMatchOrders(signedOrderLeft: SignedOrder, signedOrderRight: SignedOrder): MatchOrder { + const fill = { + left: orderUtils.getOrderWithoutExchangeAddress(signedOrderLeft), + right: orderUtils.getOrderWithoutExchangeAddress(signedOrderRight), + leftSignature: signedOrderLeft.signature, + rightSignature: signedOrderRight.signature, + }; + fill.right.makerAssetData = constants.NULL_BYTES; + fill.right.takerAssetData = constants.NULL_BYTES; + return fill; + }, +}; diff --git a/packages/contracts/test/utils/profiler.ts b/packages/contracts/test/utils/profiler.ts new file mode 100644 index 000000000..85ee24f22 --- /dev/null +++ b/packages/contracts/test/utils/profiler.ts @@ -0,0 +1,27 @@ +import { devConstants } from '@0xproject/dev-utils'; +import { ProfilerSubprovider, SolCompilerArtifactAdapter } from '@0xproject/sol-cov'; +import * as _ from 'lodash'; + +let profilerSubprovider: ProfilerSubprovider; + +export const profiler = { + start(): void { + profiler.getProfilerSubproviderSingleton().start(); + }, + stop(): void { + profiler.getProfilerSubproviderSingleton().stop(); + }, + getProfilerSubproviderSingleton(): ProfilerSubprovider { + if (_.isUndefined(profilerSubprovider)) { + profilerSubprovider = profiler._getProfilerSubprovider(); + } + return profilerSubprovider; + }, + _getProfilerSubprovider(): ProfilerSubprovider { + const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS; + const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter(); + const isVerbose = true; + const subprovider = new ProfilerSubprovider(solCompilerArtifactAdapter, defaultFromAddress, isVerbose); + return subprovider; + }, +}; diff --git a/packages/contracts/test/utils/revert_trace.ts b/packages/contracts/test/utils/revert_trace.ts new file mode 100644 index 000000000..0bf8384bc --- /dev/null +++ b/packages/contracts/test/utils/revert_trace.ts @@ -0,0 +1,21 @@ +import { devConstants } from '@0xproject/dev-utils'; +import { RevertTraceSubprovider, SolCompilerArtifactAdapter } from '@0xproject/sol-cov'; +import * as _ from 'lodash'; + +let revertTraceSubprovider: RevertTraceSubprovider; + +export const revertTrace = { + getRevertTraceSubproviderSingleton(): RevertTraceSubprovider { + if (_.isUndefined(revertTraceSubprovider)) { + revertTraceSubprovider = revertTrace._getRevertTraceSubprovider(); + } + return revertTraceSubprovider; + }, + _getRevertTraceSubprovider(): RevertTraceSubprovider { + const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS; + const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter(); + const isVerbose = true; + const subprovider = new RevertTraceSubprovider(solCompilerArtifactAdapter, defaultFromAddress, isVerbose); + return subprovider; + }, +}; diff --git a/packages/contracts/test/utils/signing_utils.ts b/packages/contracts/test/utils/signing_utils.ts new file mode 100644 index 000000000..9c711c72c --- /dev/null +++ b/packages/contracts/test/utils/signing_utils.ts @@ -0,0 +1,29 @@ +import { SignatureType } from '@0xproject/types'; +import * as ethUtil from 'ethereumjs-util'; + +export const signingUtils = { + signMessage(message: Buffer, privateKey: Buffer, signatureType: SignatureType): Buffer { + if (signatureType === SignatureType.EthSign) { + const prefixedMessage = ethUtil.hashPersonalMessage(message); + const ecSignature = ethUtil.ecsign(prefixedMessage, privateKey); + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(signatureType), + ]); + return signature; + } else if (signatureType === SignatureType.EIP712) { + const ecSignature = ethUtil.ecsign(message, privateKey); + const signature = Buffer.concat([ + ethUtil.toBuffer(ecSignature.v), + ecSignature.r, + ecSignature.s, + ethUtil.toBuffer(signatureType), + ]); + return signature; + } else { + throw new Error(`${signatureType} is not a valid signature type`); + } + }, +}; diff --git a/packages/contracts/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts b/packages/contracts/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts new file mode 100644 index 000000000..598ee6d29 --- /dev/null +++ b/packages/contracts/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts @@ -0,0 +1,19 @@ +import { AbstractBalanceAndProxyAllowanceFetcher } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; + +import { AssetWrapper } from './asset_wrapper'; + +export class SimpleAssetBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher { + private readonly _assetWrapper: AssetWrapper; + constructor(assetWrapper: AssetWrapper) { + this._assetWrapper = assetWrapper; + } + public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + const balance = await this._assetWrapper.getBalanceAsync(userAddress, assetData); + return balance; + } + public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + const proxyAllowance = await this._assetWrapper.getProxyAllowanceAsync(userAddress, assetData); + return proxyAllowance; + } +} diff --git a/packages/contracts/test/utils/simple_order_filled_cancelled_fetcher.ts b/packages/contracts/test/utils/simple_order_filled_cancelled_fetcher.ts new file mode 100644 index 000000000..ed69ecc63 --- /dev/null +++ b/packages/contracts/test/utils/simple_order_filled_cancelled_fetcher.ts @@ -0,0 +1,24 @@ +import { AbstractOrderFilledCancelledFetcher } from '@0xproject/order-utils'; +import { BigNumber } from '@0xproject/utils'; + +import { ExchangeWrapper } from './exchange_wrapper'; + +export class SimpleOrderFilledCancelledFetcher implements AbstractOrderFilledCancelledFetcher { + private readonly _exchangeWrapper: ExchangeWrapper; + private readonly _zrxAssetData: string; + constructor(exchange: ExchangeWrapper, zrxAssetData: string) { + this._exchangeWrapper = exchange; + this._zrxAssetData = zrxAssetData; + } + public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> { + const filledTakerAmount = new BigNumber(await this._exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash)); + return filledTakerAmount; + } + public async isOrderCancelledAsync(orderHash: string): Promise<boolean> { + const isCancelled = await this._exchangeWrapper.isCancelledAsync(orderHash); + return isCancelled; + } + public getZRXAssetData(): string { + return this._zrxAssetData; + } +} diff --git a/packages/contracts/test/utils/test_with_reference.ts b/packages/contracts/test/utils/test_with_reference.ts new file mode 100644 index 000000000..599b1eed4 --- /dev/null +++ b/packages/contracts/test/utils/test_with_reference.ts @@ -0,0 +1,119 @@ +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { chaiSetup } from './chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +export async function testWithReferenceFuncAsync<P0, R>( + referenceFunc: (p0: P0) => Promise<R>, + testFunc: (p0: P0) => Promise<R>, + values: [P0], +): Promise<void>; +export async function testWithReferenceFuncAsync<P0, P1, R>( + referenceFunc: (p0: P0, p1: P1) => Promise<R>, + testFunc: (p0: P0, p1: P1) => Promise<R>, + values: [P0, P1], +): Promise<void>; +export async function testWithReferenceFuncAsync<P0, P1, P2, R>( + referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>, + values: [P0, P1, P2], +): Promise<void>; +export async function testWithReferenceFuncAsync<P0, P1, P2, P3, R>( + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>, + values: [P0, P1, P2, P3], +): Promise<void>; +export async function testWithReferenceFuncAsync<P0, P1, P2, P3, P4, R>( + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>, + values: [P0, P1, P2, P3, P4], +): Promise<void>; + +/** + * Tests the behavior of a test function by comparing it to the expected + * behavior (defined by a reference function). + * + * First the reference function will be called to obtain an "expected result", + * or if the reference function throws/rejects, an "expected error". Next, the + * test function will be called to obtain an "actual result", or if the test + * function throws/rejects, an "actual error". The test passes if at least one + * of the following conditions is met: + * + * 1) Neither the reference function or the test function throw and the + * "expected result" equals the "actual result". + * + * 2) Both the reference function and the test function throw and the "actual + * error" message *contains* the "expected error" message. + * + * @param referenceFuncAsync a reference function implemented in pure + * JavaScript/TypeScript which accepts N arguments and returns the "expected + * result" or throws/rejects with the "expected error". + * @param testFuncAsync a test function which, e.g., makes a call or sends a + * transaction to a contract. It accepts the same N arguments returns the + * "actual result" or throws/rejects with the "actual error". + * @param values an array of N values, where each value corresponds in-order to + * an argument to both the test function and the reference function. + * @return A Promise that resolves if the test passes and rejects if the test + * fails, according to the rules described above. + */ +export async function testWithReferenceFuncAsync( + referenceFuncAsync: (...args: any[]) => Promise<any>, + testFuncAsync: (...args: any[]) => Promise<any>, + values: any[], +): Promise<void> { + let expectedResult: any; + let expectedErr: string | undefined; + try { + expectedResult = await referenceFuncAsync(...values); + } catch (e) { + expectedErr = e.message; + } + let actualResult: any | undefined; + try { + actualResult = await testFuncAsync(...values); + if (!_.isUndefined(expectedErr)) { + throw new Error( + `Expected error containing ${expectedErr} but got no error\n\tTest case: ${_getTestCaseString( + referenceFuncAsync, + values, + )}`, + ); + } + } catch (e) { + if (_.isUndefined(expectedErr)) { + throw new Error(`${e.message}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`); + } else { + expect(e.message).to.contain( + expectedErr, + `${e.message}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`, + ); + } + } + if (!_.isUndefined(actualResult) && !_.isUndefined(expectedResult)) { + expect(actualResult).to.deep.equal( + expectedResult, + `Test case: ${_getTestCaseString(referenceFuncAsync, values)}`, + ); + } +} + +function _getTestCaseString(referenceFuncAsync: (...args: any[]) => Promise<any>, values: any[]): string { + const paramNames = _getParameterNames(referenceFuncAsync); + return JSON.stringify(_.zipObject(paramNames, values)); +} + +// Source: https://stackoverflow.com/questions/1007981/how-to-get-function-parameter-names-values-dynamically +function _getParameterNames(func: (...args: any[]) => any): string[] { + return _.toString(func) + .replace(/[/][/].*$/gm, '') // strip single-line comments + .replace(/\s+/g, '') // strip white space + .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments + .split('){', 1)[0] + .replace(/^[^(]*[(]/, '') // extract the parameters + .replace(/=[^,]+/g, '') // strip any ES6 defaults + .split(',') + .filter(Boolean); // split & filter [""] +} diff --git a/packages/contracts/util/token_registry_wrapper.ts b/packages/contracts/test/utils/token_registry_wrapper.ts index d78c8a64e..f1c40e8ff 100644 --- a/packages/contracts/util/token_registry_wrapper.ts +++ b/packages/contracts/test/utils/token_registry_wrapper.ts @@ -1,16 +1,21 @@ -import * as Web3 from 'web3'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider } from 'ethereum-types'; -import { TokenRegistryContract } from '../src/contract_wrappers/generated/token_registry'; +import { TokenRegistryContract } from '../../generated_contract_wrappers/token_registry'; import { Token } from './types'; +import { constants } from './constants'; + export class TokenRegWrapper { - private _tokenReg: TokenRegistryContract; - constructor(tokenRegContract: TokenRegistryContract) { + private readonly _tokenReg: TokenRegistryContract; + private readonly _web3Wrapper: Web3Wrapper; + constructor(tokenRegContract: TokenRegistryContract, provider: Provider) { this._tokenReg = tokenRegContract; + this._web3Wrapper = new Web3Wrapper(provider); } - public async addTokenAsync(token: Token, from: string) { - const tx = this._tokenReg.addToken.sendTransactionAsync( + public async addTokenAsync(token: Token, from: string): Promise<string> { + const txHash = await this._tokenReg.addToken.sendTransactionAsync( token.address as string, token.name, token.symbol, @@ -19,9 +24,10 @@ export class TokenRegWrapper { token.swarmHash, { from }, ); - return tx; + await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS); + return txHash; } - public async getTokenMetaDataAsync(tokenAddress: string) { + public async getTokenMetaDataAsync(tokenAddress: string): Promise<Token> { const data = await this._tokenReg.getTokenMetaData.callAsync(tokenAddress); const token: Token = { address: data[0], @@ -33,7 +39,7 @@ export class TokenRegWrapper { }; return token; } - public async getTokenByNameAsync(tokenName: string) { + public async getTokenByNameAsync(tokenName: string): Promise<Token> { const data = await this._tokenReg.getTokenByName.callAsync(tokenName); const token: Token = { address: data[0], @@ -45,7 +51,7 @@ export class TokenRegWrapper { }; return token; } - public async getTokenBySymbolAsync(tokenSymbol: string) { + public async getTokenBySymbolAsync(tokenSymbol: string): Promise<Token> { const data = await this._tokenReg.getTokenBySymbol.callAsync(tokenSymbol); const token: Token = { address: data[0], diff --git a/packages/contracts/test/utils/transaction_factory.ts b/packages/contracts/test/utils/transaction_factory.ts new file mode 100644 index 000000000..281c1e30d --- /dev/null +++ b/packages/contracts/test/utils/transaction_factory.ts @@ -0,0 +1,47 @@ +import { EIP712Schema, EIP712Types, EIP712Utils, generatePseudoRandomSalt } from '@0xproject/order-utils'; +import { SignatureType } from '@0xproject/types'; +import * as ethUtil from 'ethereumjs-util'; + +import { signingUtils } from './signing_utils'; +import { SignedTransaction } from './types'; + +const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = { + name: 'ZeroExTransaction', + parameters: [ + { name: 'salt', type: EIP712Types.Uint256 }, + { name: 'signerAddress', type: EIP712Types.Address }, + { name: 'data', type: EIP712Types.Bytes }, + ], +}; + +export class TransactionFactory { + private readonly _signerBuff: Buffer; + private readonly _exchangeAddress: string; + private readonly _privateKey: Buffer; + constructor(privateKey: Buffer, exchangeAddress: string) { + this._privateKey = privateKey; + this._exchangeAddress = exchangeAddress; + this._signerBuff = ethUtil.privateToAddress(this._privateKey); + } + public newSignedTransaction(data: string, signatureType: SignatureType = SignatureType.EthSign): SignedTransaction { + const salt = generatePseudoRandomSalt(); + const signerAddress = `0x${this._signerBuff.toString('hex')}`; + const executeTransactionData = { + salt, + signerAddress, + data, + }; + const executeTransactionHashBuff = EIP712Utils.structHash( + EIP712_ZEROEX_TRANSACTION_SCHEMA, + executeTransactionData, + ); + const txHash = EIP712Utils.createEIP712Message(executeTransactionHashBuff, this._exchangeAddress); + const signature = signingUtils.signMessage(txHash, this._privateKey, signatureType); + const signedTx = { + exchangeAddress: this._exchangeAddress, + signature: `0x${signature.toString('hex')}`, + ...executeTransactionData, + }; + return signedTx; + } +} diff --git a/packages/contracts/test/utils/type_encoding_utils.ts b/packages/contracts/test/utils/type_encoding_utils.ts new file mode 100644 index 000000000..75307b9bd --- /dev/null +++ b/packages/contracts/test/utils/type_encoding_utils.ts @@ -0,0 +1,21 @@ +import { BigNumber } from '@0xproject/utils'; +import BN = require('bn.js'); +import ethUtil = require('ethereumjs-util'); + +import { constants } from './constants'; + +export const typeEncodingUtils = { + encodeUint256(value: BigNumber): Buffer { + const base = 10; + const formattedValue = new BN(value.toString(base)); + const encodedValue = ethUtil.toBuffer(formattedValue); + // tslint:disable-next-line:custom-no-magic-numbers + const paddedValue = ethUtil.setLengthLeft(encodedValue, constants.WORD_LENGTH); + return paddedValue; + }, + decodeUint256(encodedValue: Buffer): BigNumber { + const formattedValue = ethUtil.bufferToHex(encodedValue); + const value = new BigNumber(formattedValue, constants.BASE_16); + return value; + }, +}; diff --git a/packages/contracts/test/utils/types.ts b/packages/contracts/test/utils/types.ts new file mode 100644 index 000000000..67313b647 --- /dev/null +++ b/packages/contracts/test/utils/types.ts @@ -0,0 +1,237 @@ +import { OrderWithoutExchangeAddress } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { AbiDefinition } from 'ethereum-types'; + +export interface ERC20BalancesByOwner { + [ownerAddress: string]: { + [tokenAddress: string]: BigNumber; + }; +} + +export interface ERC721TokenIdsByOwner { + [ownerAddress: string]: { + [tokenAddress: string]: BigNumber[]; + }; +} + +export interface SubmissionContractEventArgs { + transactionId: BigNumber; +} + +export interface BatchFillOrders { + orders: OrderWithoutExchangeAddress[]; + signatures: string[]; + takerAssetFillAmounts: BigNumber[]; +} + +export interface MarketSellOrders { + orders: OrderWithoutExchangeAddress[]; + signatures: string[]; + takerAssetFillAmount: BigNumber; +} + +export interface MarketBuyOrders { + orders: OrderWithoutExchangeAddress[]; + signatures: string[]; + makerAssetFillAmount: BigNumber; +} + +export interface BatchCancelOrders { + orders: OrderWithoutExchangeAddress[]; +} + +export interface CancelOrdersBefore { + salt: BigNumber; +} + +export interface TransactionDataParams { + name: string; + abi: AbiDefinition[]; + args: any[]; +} + +export interface MultiSigConfig { + owners: string[]; + confirmationsRequired: number; + secondsRequired: number; +} + +export interface MultiSigConfigByNetwork { + [networkName: string]: MultiSigConfig; +} + +export interface Token { + address?: string; + name: string; + symbol: string; + decimals: number; + ipfsHash: string; + swarmHash: string; +} + +export enum OrderStatus { + INVALID, + INVALID_MAKER_ASSET_AMOUNT, + INVALID_TAKER_ASSET_AMOUNT, + FILLABLE, + EXPIRED, + FULLY_FILLED, + CANCELLED, +} + +export enum ContractName { + TokenRegistry = 'TokenRegistry', + MultiSigWalletWithTimeLock = 'MultiSigWalletWithTimeLock', + Exchange = 'Exchange', + ZRXToken = 'ZRXToken', + DummyERC20Token = 'DummyERC20Token', + EtherToken = 'WETH9', + AssetProxyOwner = 'AssetProxyOwner', + AccountLevels = 'AccountLevels', + EtherDelta = 'EtherDelta', + Arbitrage = 'Arbitrage', + TestAssetDataDecoders = 'TestAssetDataDecoders', + TestAssetProxyDispatcher = 'TestAssetProxyDispatcher', + TestLibs = 'TestLibs', + TestSignatureValidator = 'TestSignatureValidator', + ERC20Proxy = 'ERC20Proxy', + ERC721Proxy = 'ERC721Proxy', + DummyERC721Receiver = 'DummyERC721Receiver', + DummyERC721Token = 'DummyERC721Token', + TestLibBytes = 'TestLibBytes', + TestWallet = 'TestWallet', + Authorizable = 'Authorizable', + Whitelist = 'Whitelist', + Forwarder = 'Forwarder', +} + +export interface SignedTransaction { + exchangeAddress: string; + salt: BigNumber; + signerAddress: string; + data: string; + signature: string; +} + +export interface TransferAmountsByMatchOrders { + // Left Maker + amountBoughtByLeftMaker: BigNumber; + amountSoldByLeftMaker: BigNumber; + amountReceivedByLeftMaker: BigNumber; + feePaidByLeftMaker: BigNumber; + // Right Maker + amountBoughtByRightMaker: BigNumber; + amountSoldByRightMaker: BigNumber; + amountReceivedByRightMaker: BigNumber; + feePaidByRightMaker: BigNumber; + // Taker + amountReceivedByTaker: BigNumber; + feePaidByTakerLeft: BigNumber; + feePaidByTakerRight: BigNumber; + totalFeePaidByTaker: BigNumber; + // Fee Recipients + feeReceivedLeft: BigNumber; + feeReceivedRight: BigNumber; +} + +export interface OrderInfo { + orderStatus: number; + orderHash: string; + orderTakerAssetFilledAmount: BigNumber; +} + +export interface CancelOrder { + order: OrderWithoutExchangeAddress; + takerAssetCancelAmount: BigNumber; +} + +export interface MatchOrder { + left: OrderWithoutExchangeAddress; + right: OrderWithoutExchangeAddress; + leftSignature: string; + rightSignature: string; +} + +// Combinatorial testing types + +export enum FeeRecipientAddressScenario { + BurnAddress = 'BURN_ADDRESS', + EthUserAddress = 'ETH_USER_ADDRESS', +} + +export enum OrderAssetAmountScenario { + Zero = 'ZERO', + Large = 'LARGE', + Small = 'SMALL', +} + +export enum TakerScenario { + CorrectlySpecified = 'CORRECTLY_SPECIFIED', + IncorrectlySpecified = 'INCORRECTLY_SPECIFIED', + Unspecified = 'UNSPECIFIED', +} + +export enum ExpirationTimeSecondsScenario { + InPast = 'IN_PAST', + InFuture = 'IN_FUTURE', +} + +export enum AssetDataScenario { + ERC721 = 'ERC721', + ZRXFeeToken = 'ZRX_FEE_TOKEN', + ERC20FiveDecimals = 'ERC20_FIVE_DECIMALS', + ERC20NonZRXEighteenDecimals = 'ERC20_NON_ZRX_EIGHTEEN_DECIMALS', +} + +export enum TakerAssetFillAmountScenario { + Zero = 'ZERO', + GreaterThanRemainingFillableTakerAssetAmount = 'GREATER_THAN_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT', + LessThanRemainingFillableTakerAssetAmount = 'LESS_THAN_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT', + ExactlyRemainingFillableTakerAssetAmount = 'EXACTLY_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT', +} + +export interface OrderScenario { + takerScenario: TakerScenario; + feeRecipientScenario: FeeRecipientAddressScenario; + makerAssetAmountScenario: OrderAssetAmountScenario; + takerAssetAmountScenario: OrderAssetAmountScenario; + makerFeeScenario: OrderAssetAmountScenario; + takerFeeScenario: OrderAssetAmountScenario; + expirationTimeSecondsScenario: ExpirationTimeSecondsScenario; + makerAssetDataScenario: AssetDataScenario; + takerAssetDataScenario: AssetDataScenario; +} + +export enum BalanceAmountScenario { + Exact = 'EXACT', + TooLow = 'TOO_LOW', + Higher = 'HIGHER', +} + +export enum AllowanceAmountScenario { + Exact = 'EXACT', + TooLow = 'TOO_LOW', + Higher = 'HIGHER', + Unlimited = 'UNLIMITED', +} + +export interface TraderStateScenario { + traderAssetBalance: BalanceAmountScenario; + traderAssetAllowance: AllowanceAmountScenario; + zrxFeeBalance: BalanceAmountScenario; + zrxFeeAllowance: AllowanceAmountScenario; +} + +export interface FillScenario { + orderScenario: OrderScenario; + takerAssetFillAmountScenario: TakerAssetFillAmountScenario; + makerStateScenario: TraderStateScenario; + takerStateScenario: TraderStateScenario; +} + +export interface FillResults { + makerAssetFilledAmount: BigNumber; + takerAssetFilledAmount: BigNumber; + makerFeePaid: BigNumber; + takerFeePaid: BigNumber; +} diff --git a/packages/contracts/test/utils/web3_wrapper.ts b/packages/contracts/test/utils/web3_wrapper.ts index 35a8095a6..acb3103b7 100644 --- a/packages/contracts/test/utils/web3_wrapper.ts +++ b/packages/contracts/test/utils/web3_wrapper.ts @@ -1,6 +1,84 @@ -import { web3Factory } from '@0xproject/dev-utils'; +import { devConstants, env, EnvVars, web3Factory } from '@0xproject/dev-utils'; +import { prependSubprovider } from '@0xproject/subproviders'; +import { logUtils } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; -const web3ProviderConfig = { shouldUseInProcessGanache: true }; -export const web3 = web3Factory.create(web3ProviderConfig); -export const web3Wrapper = new Web3Wrapper(web3.currentProvider); +import { coverage } from './coverage'; +import { profiler } from './profiler'; +import { revertTrace } from './revert_trace'; + +enum ProviderType { + Ganache = 'ganache', + Geth = 'geth', +} + +let testProvider: ProviderType; +switch (process.env.TEST_PROVIDER) { + case undefined: + testProvider = ProviderType.Ganache; + break; + case 'ganache': + testProvider = ProviderType.Ganache; + break; + case 'geth': + testProvider = ProviderType.Geth; + break; + default: + throw new Error(`Unknown TEST_PROVIDER: ${process.env.TEST_PROVIDER}`); +} + +const ganacheTxDefaults = { + from: devConstants.TESTRPC_FIRST_ADDRESS, + gas: devConstants.GAS_LIMIT, +}; +const gethTxDefaults = { + from: devConstants.TESTRPC_FIRST_ADDRESS, +}; +export const txDefaults = testProvider === ProviderType.Ganache ? ganacheTxDefaults : gethTxDefaults; + +const gethConfigs = { + shouldUseInProcessGanache: false, + rpcUrl: 'http://localhost:8501', + shouldUseFakeGasEstimate: false, +}; +const ganacheConfigs = { + shouldUseInProcessGanache: true, +}; +const providerConfigs = testProvider === ProviderType.Ganache ? ganacheConfigs : gethConfigs; + +export const provider = web3Factory.getRpcProvider(providerConfigs); +const isCoverageEnabled = env.parseBoolean(EnvVars.SolidityCoverage); +const isProfilerEnabled = env.parseBoolean(EnvVars.SolidityProfiler); +const isRevertTraceEnabled = env.parseBoolean(EnvVars.SolidityRevertTrace); +const enabledSubproviderCount = _.filter( + [isCoverageEnabled, isProfilerEnabled, isRevertTraceEnabled], + _.identity.bind(_), +).length; +if (enabledSubproviderCount > 1) { + throw new Error(`Only one of coverage, profiler, or revert trace subproviders can be enabled at a time`); +} +if (isCoverageEnabled) { + const coverageSubprovider = coverage.getCoverageSubproviderSingleton(); + prependSubprovider(provider, coverageSubprovider); +} +if (isProfilerEnabled) { + if (testProvider === ProviderType.Ganache) { + logUtils.warn( + "Gas costs in Ganache traces are incorrect and we don't recommend using it for profiling. Please switch to Geth", + ); + process.exit(1); + } + const profilerSubprovider = profiler.getProfilerSubproviderSingleton(); + logUtils.log( + "By default profilerSubprovider is stopped so that you don't get noise from setup code. Don't forget to start it before the code you want to profile and stop it afterwards", + ); + profilerSubprovider.stop(); + prependSubprovider(provider, profilerSubprovider); +} +if (isRevertTraceEnabled) { + const revertTraceSubprovider = revertTrace.getRevertTraceSubproviderSingleton(); + prependSubprovider(provider, revertTraceSubprovider); +} + +export const web3Wrapper = new Web3Wrapper(provider); diff --git a/packages/contracts/test/zrx_token.ts b/packages/contracts/test/zrx_token.ts deleted file mode 100644 index a4cc3c2fc..000000000 --- a/packages/contracts/test/zrx_token.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { ZeroEx } from '0x.js'; -import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import * as Web3 from 'web3'; - -import { ZRXTokenContract } from '../src/contract_wrappers/generated/zrx_token'; -import { constants } from '../util/constants'; -import { ContractName } from '../util/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { deployer } from './utils/deployer'; -import { web3, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('ZRXToken', () => { - let owner: string; - let spender: string; - let zeroEx: ZeroEx; - - let MAX_UINT: BigNumber; - - let zrx: ZRXTokenContract; - let zrxAddress: string; - - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - owner = accounts[0]; - spender = accounts[1]; - zeroEx = new ZeroEx(web3.currentProvider, { - networkId: constants.TESTRPC_NETWORK_ID, - }); - const zrxInstance = await deployer.deployAsync(ContractName.ZRXToken); - zrx = new ZRXTokenContract(web3Wrapper, zrxInstance.abi, zrxInstance.address); - zrxAddress = zrx.address; - MAX_UINT = zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('constants', () => { - it('should have 18 decimals', async () => { - const decimals = new BigNumber(await zrx.decimals.callAsync()); - const expectedDecimals = 18; - expect(decimals).to.be.bignumber.equal(expectedDecimals); - }); - - it('should have a total supply of 1 billion tokens', async () => { - const totalSupply = new BigNumber(await zrx.totalSupply.callAsync()); - const expectedTotalSupply = 1000000000; - expect(ZeroEx.toUnitAmount(totalSupply, 18)).to.be.bignumber.equal(expectedTotalSupply); - }); - - it('should be named 0x Protocol Token', async () => { - const name = await zrx.name.callAsync(); - const expectedName = '0x Protocol Token'; - expect(name).to.be.equal(expectedName); - }); - - it('should have the symbol ZRX', async () => { - const symbol = await zrx.symbol.callAsync(); - const expectedSymbol = 'ZRX'; - expect(symbol).to.be.equal(expectedSymbol); - }); - }); - - describe('constructor', () => { - it('should initialize owner balance to totalSupply', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const totalSupply = new BigNumber(await zrx.totalSupply.callAsync()); - expect(totalSupply).to.be.bignumber.equal(ownerBalance); - }); - }); - - describe('transfer', () => { - it('should transfer balance from sender to receiver', async () => { - const receiver = spender; - const initOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const amountToTransfer = new BigNumber(1); - await zeroEx.token.transferAsync(zrxAddress, owner, receiver, amountToTransfer); - const finalOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const finalReceiverBalance = await zeroEx.token.getBalanceAsync(zrxAddress, receiver); - - const expectedFinalOwnerBalance = initOwnerBalance.minus(amountToTransfer); - const expectedFinalReceiverBalance = amountToTransfer; - expect(finalOwnerBalance).to.be.bignumber.equal(expectedFinalOwnerBalance); - expect(finalReceiverBalance).to.be.bignumber.equal(expectedFinalReceiverBalance); - }); - - it('should return true on a 0 value transfer', async () => { - const didReturnTrue = await zrx.transfer.callAsync(spender, new BigNumber(0), { - from: owner, - }); - expect(didReturnTrue).to.be.true(); - }); - }); - - describe('transferFrom', () => { - it('should return false if owner has insufficient balance', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const amountToTransfer = ownerBalance.plus(1); - await zeroEx.token.setAllowanceAsync(zrxAddress, owner, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_APPROVE_GAS, - }); - const didReturnTrue = await zrx.transferFrom.callAsync(owner, spender, amountToTransfer, { from: spender }); - expect(didReturnTrue).to.be.false(); - }); - - it('should return false if spender has insufficient allowance', async () => { - const ownerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const amountToTransfer = ownerBalance; - - const spenderAllowance = await zeroEx.token.getAllowanceAsync(zrxAddress, owner, spender); - const spenderAllowanceIsInsufficient = spenderAllowance.cmp(amountToTransfer) < 0; - expect(spenderAllowanceIsInsufficient).to.be.true(); - - const didReturnTrue = await zrx.transferFrom.callAsync(owner, spender, amountToTransfer, { from: spender }); - expect(didReturnTrue).to.be.false(); - }); - - it('should return true on a 0 value transfer', async () => { - const amountToTransfer = new BigNumber(0); - const didReturnTrue = await zrx.transferFrom.callAsync(owner, spender, amountToTransfer, { from: spender }); - expect(didReturnTrue).to.be.true(); - }); - - it('should not modify spender allowance if spender allowance is 2^256 - 1', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const amountToTransfer = initOwnerBalance; - const initSpenderAllowance = MAX_UINT; - await zeroEx.token.setAllowanceAsync(zrxAddress, owner, spender, initSpenderAllowance, { - gasLimit: constants.MAX_TOKEN_APPROVE_GAS, - }); - await zeroEx.token.transferFromAsync(zrxAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newSpenderAllowance = await zeroEx.token.getAllowanceAsync(zrxAddress, owner, spender); - expect(initSpenderAllowance).to.be.bignumber.equal(newSpenderAllowance); - }); - - it('should transfer the correct balances if spender has sufficient allowance', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const initSpenderBalance = await zeroEx.token.getBalanceAsync(zrxAddress, spender); - const amountToTransfer = initOwnerBalance; - const initSpenderAllowance = initOwnerBalance; - await zeroEx.token.setAllowanceAsync(zrxAddress, owner, spender, initSpenderAllowance); - await zeroEx.token.transferFromAsync(zrxAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const newSpenderBalance = await zeroEx.token.getBalanceAsync(zrxAddress, spender); - - expect(newOwnerBalance).to.be.bignumber.equal(0); - expect(newSpenderBalance).to.be.bignumber.equal(initSpenderBalance.plus(initOwnerBalance)); - }); - - it('should modify allowance if spender has sufficient allowance less than 2^256 - 1', async () => { - const initOwnerBalance = await zeroEx.token.getBalanceAsync(zrxAddress, owner); - const amountToTransfer = initOwnerBalance; - await zeroEx.token.setAllowanceAsync(zrxAddress, owner, spender, amountToTransfer); - await zeroEx.token.transferFromAsync(zrxAddress, owner, spender, spender, amountToTransfer, { - gasLimit: constants.MAX_TOKEN_TRANSFERFROM_GAS, - }); - - const newSpenderAllowance = await zeroEx.token.getAllowanceAsync(zrxAddress, owner, spender); - expect(newSpenderAllowance).to.be.bignumber.equal(0); - }); - }); -}); diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json index f32a3682a..86b33ede7 100644 --- a/packages/contracts/tsconfig.json +++ b/packages/contracts/tsconfig.json @@ -4,15 +4,13 @@ "outDir": "lib", "baseUrl": ".", "declaration": false, - "noImplicitThis": false, "allowJs": true }, "include": [ - "../../node_modules/types-ethereumjs-util/index.d.ts", - "../../node_modules/types-bn/index.d.ts", "./globals.d.ts", + "./contract_wrappers", "./src/**/*", - "./util/**/*", + "./utils/**/*", "./test/**/*", "./migrations/**/*" ], diff --git a/packages/contracts/tslint.json b/packages/contracts/tslint.json index ffaefe83a..1ab924e47 100644 --- a/packages/contracts/tslint.json +++ b/packages/contracts/tslint.json @@ -1,3 +1,6 @@ { - "extends": ["@0xproject/tslint-config"] + "extends": ["@0xproject/tslint-config"], + "rules": { + "custom-no-magic-numbers": false + } } diff --git a/packages/contracts/util/artifacts.ts b/packages/contracts/util/artifacts.ts deleted file mode 100644 index 7c375c1b7..000000000 --- a/packages/contracts/util/artifacts.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as DummyTokenArtifact from '../src/artifacts/DummyToken.json'; -import * as ExchangeArtifact from '../src/artifacts/Exchange.json'; -import * as MaliciousTokenArtifact from '../src/artifacts/MaliciousToken.json'; -import * as MultiSigWalletWithTimeLockArtifact from '../src/artifacts/MultiSigWalletWithTimeLock.json'; -import * as MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressArtifact from '../src/artifacts/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.json'; -import * as TokenArtifact from '../src/artifacts/Token.json'; -import * as TokenRegistryArtifact from '../src/artifacts/TokenRegistry.json'; -import * as TokenTransferProxyArtifact from '../src/artifacts/TokenTransferProxy.json'; -import * as EtherTokenArtifact from '../src/artifacts/WETH9.json'; -import * as ZRXArtifact from '../src/artifacts/ZRXToken.json'; - -import { Artifact } from './types'; - -export const artifacts = { - ZRXArtifact: (ZRXArtifact as any) as Artifact, - DummyTokenArtifact: (DummyTokenArtifact as any) as Artifact, - TokenArtifact: (TokenArtifact as any) as Artifact, - ExchangeArtifact: (ExchangeArtifact as any) as Artifact, - EtherTokenArtifact: (EtherTokenArtifact as any) as Artifact, - TokenRegistryArtifact: (TokenRegistryArtifact as any) as Artifact, - MaliciousTokenArtifact: (MaliciousTokenArtifact as any) as Artifact, - TokenTransferProxyArtifact: (TokenTransferProxyArtifact as any) as Artifact, - MultiSigWalletWithTimeLockArtifact: (MultiSigWalletWithTimeLockArtifact as any) as Artifact, - MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressArtifact: (MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressArtifact as any) as Artifact, -}; diff --git a/packages/contracts/util/balances.ts b/packages/contracts/util/balances.ts deleted file mode 100644 index d03d4b3c5..000000000 --- a/packages/contracts/util/balances.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { BigNumber } from '@0xproject/utils'; -import * as _ from 'lodash'; -import * as Web3 from 'web3'; - -import { DummyTokenContract } from '../src/contract_wrappers/generated/dummy_token'; - -import { BalancesByOwner } from './types'; - -export class Balances { - private _tokenContractInstances: DummyTokenContract[]; - private _ownerAddresses: string[]; - constructor(tokenContractInstances: DummyTokenContract[], ownerAddresses: string[]) { - this._tokenContractInstances = tokenContractInstances; - this._ownerAddresses = ownerAddresses; - } - public async getAsync(): Promise<BalancesByOwner> { - const balancesByOwner: BalancesByOwner = {}; - for (const tokenContractInstance of this._tokenContractInstances) { - for (const ownerAddress of this._ownerAddresses) { - let balance = await tokenContractInstance.balanceOf.callAsync(ownerAddress); - balance = new BigNumber(balance); - if (_.isUndefined(balancesByOwner[ownerAddress])) { - balancesByOwner[ownerAddress] = {}; - } - balancesByOwner[ownerAddress][tokenContractInstance.address] = balance; - } - } - return balancesByOwner; - } -} diff --git a/packages/contracts/util/constants.ts b/packages/contracts/util/constants.ts deleted file mode 100644 index 9bb090a2a..000000000 --- a/packages/contracts/util/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -const DUMMY_TOKEN_NAME = ''; -const DUMMY_TOKEN_SYMBOL = ''; -const DUMMY_TOKEN_DECIMALS = 18; -const DUMMY_TOKEN_TOTAL_SUPPLY = 0; - -export const constants = { - NULL_BYTES: '0x', - INVALID_OPCODE: 'invalid opcode', - REVERT: 'revert', - TESTRPC_NETWORK_ID: 50, - MAX_ETHERTOKEN_WITHDRAW_GAS: 43000, - MAX_TOKEN_TRANSFERFROM_GAS: 80000, - MAX_TOKEN_APPROVE_GAS: 60000, - DUMMY_TOKEN_ARGS: [DUMMY_TOKEN_NAME, DUMMY_TOKEN_SYMBOL, DUMMY_TOKEN_DECIMALS, DUMMY_TOKEN_TOTAL_SUPPLY], -}; diff --git a/packages/contracts/util/crypto.ts b/packages/contracts/util/crypto.ts deleted file mode 100644 index 97b8f5643..000000000 --- a/packages/contracts/util/crypto.ts +++ /dev/null @@ -1,43 +0,0 @@ -import BN = require('bn.js'); -import ABI = require('ethereumjs-abi'); -import ethUtil = require('ethereumjs-util'); -import * as _ from 'lodash'; - -export const crypto = { - /* - * We convert types from JS to Solidity as follows: - * BigNumber -> uint256 - * number -> uint8 - * string -> string - * boolean -> bool - * valid Ethereum address -> address - */ - solSHA3(args: any[]): Buffer { - return crypto._solHash(args, ABI.soliditySHA3); - }, - solSHA256(args: any[]): Buffer { - return crypto._solHash(args, ABI.soliditySHA256); - }, - _solHash(args: any[], hashFunction: (types: string[], values: any[]) => Buffer) { - const argTypes: string[] = []; - _.each(args, (arg, i) => { - const isNumber = _.isFinite(arg); - if (isNumber) { - argTypes.push('uint8'); - } else if (arg.isBigNumber) { - argTypes.push('uint256'); - args[i] = new BN(arg.toString(10), 10); - } else if (ethUtil.isValidAddress(arg)) { - argTypes.push('address'); - } else if (_.isString(arg)) { - argTypes.push('string'); - } else if (_.isBoolean(arg)) { - argTypes.push('bool'); - } else { - throw new Error(`Unable to guess arg type: ${arg}`); - } - }); - const hash = hashFunction(argTypes, args); - return hash; - }, -}; diff --git a/packages/contracts/util/exchange_wrapper.ts b/packages/contracts/util/exchange_wrapper.ts deleted file mode 100644 index f016067fe..000000000 --- a/packages/contracts/util/exchange_wrapper.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { SignedOrder, TransactionReceiptWithDecodedLogs, ZeroEx } from '0x.js'; -import { BigNumber } from '@0xproject/utils'; -import * as _ from 'lodash'; -import * as Web3 from 'web3'; - -import { ExchangeContract } from '../src/contract_wrappers/generated/exchange'; - -import { formatters } from './formatters'; -import { signedOrderUtils } from './signed_order_utils'; - -export class ExchangeWrapper { - private _exchange: ExchangeContract; - private _zeroEx: ZeroEx; - constructor(exchangeContract: ExchangeContract, zeroEx: ZeroEx) { - this._exchange = exchangeContract; - this._zeroEx = zeroEx; - } - public async fillOrderAsync( - signedOrder: SignedOrder, - from: string, - opts: { - fillTakerTokenAmount?: BigNumber; - shouldThrowOnInsufficientBalanceOrAllowance?: boolean; - } = {}, - ): Promise<TransactionReceiptWithDecodedLogs> { - const shouldThrowOnInsufficientBalanceOrAllowance = !!opts.shouldThrowOnInsufficientBalanceOrAllowance; - const params = signedOrderUtils.createFill( - signedOrder, - shouldThrowOnInsufficientBalanceOrAllowance, - opts.fillTakerTokenAmount, - ); - const txHash = await this._exchange.fillOrder.sendTransactionAsync( - params.orderAddresses, - params.orderValues, - params.fillTakerTokenAmount, - params.shouldThrowOnInsufficientBalanceOrAllowance, - params.v, - params.r, - params.s, - { from }, - ); - const tx = await this._zeroEx.awaitTransactionMinedAsync(txHash); - tx.logs = _.filter(tx.logs, log => log.address === this._exchange.address); - _.each(tx.logs, log => wrapLogBigNumbers(log)); - return tx; - } - public async cancelOrderAsync( - signedOrder: SignedOrder, - from: string, - opts: { cancelTakerTokenAmount?: BigNumber } = {}, - ): Promise<TransactionReceiptWithDecodedLogs> { - const params = signedOrderUtils.createCancel(signedOrder, opts.cancelTakerTokenAmount); - const txHash = await this._exchange.cancelOrder.sendTransactionAsync( - params.orderAddresses, - params.orderValues, - params.cancelTakerTokenAmount, - { from }, - ); - const tx = await this._zeroEx.awaitTransactionMinedAsync(txHash); - tx.logs = _.filter(tx.logs, log => log.address === this._exchange.address); - _.each(tx.logs, log => wrapLogBigNumbers(log)); - return tx; - } - public async fillOrKillOrderAsync( - signedOrder: SignedOrder, - from: string, - opts: { fillTakerTokenAmount?: BigNumber } = {}, - ): Promise<TransactionReceiptWithDecodedLogs> { - const shouldThrowOnInsufficientBalanceOrAllowance = true; - const params = signedOrderUtils.createFill( - signedOrder, - shouldThrowOnInsufficientBalanceOrAllowance, - opts.fillTakerTokenAmount, - ); - const txHash = await this._exchange.fillOrKillOrder.sendTransactionAsync( - params.orderAddresses, - params.orderValues, - params.fillTakerTokenAmount, - params.v, - params.r, - params.s, - { from }, - ); - const tx = await this._zeroEx.awaitTransactionMinedAsync(txHash); - tx.logs = _.filter(tx.logs, log => log.address === this._exchange.address); - _.each(tx.logs, log => wrapLogBigNumbers(log)); - return tx; - } - public async batchFillOrdersAsync( - orders: SignedOrder[], - from: string, - opts: { - fillTakerTokenAmounts?: BigNumber[]; - shouldThrowOnInsufficientBalanceOrAllowance?: boolean; - } = {}, - ): Promise<TransactionReceiptWithDecodedLogs> { - const shouldThrowOnInsufficientBalanceOrAllowance = !!opts.shouldThrowOnInsufficientBalanceOrAllowance; - const params = formatters.createBatchFill( - orders, - shouldThrowOnInsufficientBalanceOrAllowance, - opts.fillTakerTokenAmounts, - ); - const txHash = await this._exchange.batchFillOrders.sendTransactionAsync( - params.orderAddresses, - params.orderValues, - params.fillTakerTokenAmounts, - params.shouldThrowOnInsufficientBalanceOrAllowance, - params.v, - params.r, - params.s, - { from }, - ); - const tx = await this._zeroEx.awaitTransactionMinedAsync(txHash); - tx.logs = _.filter(tx.logs, log => log.address === this._exchange.address); - _.each(tx.logs, log => wrapLogBigNumbers(log)); - return tx; - } - public async batchFillOrKillOrdersAsync( - orders: SignedOrder[], - from: string, - opts: { fillTakerTokenAmounts?: BigNumber[]; shouldThrowOnInsufficientBalanceOrAllowance?: boolean } = {}, - ): Promise<TransactionReceiptWithDecodedLogs> { - const shouldThrowOnInsufficientBalanceOrAllowance = !!opts.shouldThrowOnInsufficientBalanceOrAllowance; - const params = formatters.createBatchFill( - orders, - shouldThrowOnInsufficientBalanceOrAllowance, - opts.fillTakerTokenAmounts, - ); - const txHash = await this._exchange.batchFillOrKillOrders.sendTransactionAsync( - params.orderAddresses, - params.orderValues, - params.fillTakerTokenAmounts, - params.v, - params.r, - params.s, - { from }, - ); - const tx = await this._zeroEx.awaitTransactionMinedAsync(txHash); - tx.logs = _.filter(tx.logs, log => log.address === this._exchange.address); - _.each(tx.logs, log => wrapLogBigNumbers(log)); - return tx; - } - public async fillOrdersUpToAsync( - orders: SignedOrder[], - from: string, - opts: { fillTakerTokenAmount: BigNumber; shouldThrowOnInsufficientBalanceOrAllowance?: boolean }, - ): Promise<TransactionReceiptWithDecodedLogs> { - const shouldThrowOnInsufficientBalanceOrAllowance = !!opts.shouldThrowOnInsufficientBalanceOrAllowance; - const params = formatters.createFillUpTo( - orders, - shouldThrowOnInsufficientBalanceOrAllowance, - opts.fillTakerTokenAmount, - ); - const txHash = await this._exchange.fillOrdersUpTo.sendTransactionAsync( - params.orderAddresses, - params.orderValues, - params.fillTakerTokenAmount, - params.shouldThrowOnInsufficientBalanceOrAllowance, - params.v, - params.r, - params.s, - { from }, - ); - const tx = await this._zeroEx.awaitTransactionMinedAsync(txHash); - tx.logs = _.filter(tx.logs, log => log.address === this._exchange.address); - _.each(tx.logs, log => wrapLogBigNumbers(log)); - return tx; - } - public async batchCancelOrdersAsync( - orders: SignedOrder[], - from: string, - opts: { cancelTakerTokenAmounts?: BigNumber[] } = {}, - ): Promise<TransactionReceiptWithDecodedLogs> { - const params = formatters.createBatchCancel(orders, opts.cancelTakerTokenAmounts); - const txHash = await this._exchange.batchCancelOrders.sendTransactionAsync( - params.orderAddresses, - params.orderValues, - params.cancelTakerTokenAmounts, - { from }, - ); - const tx = await this._zeroEx.awaitTransactionMinedAsync(txHash); - tx.logs = _.filter(tx.logs, log => log.address === this._exchange.address); - _.each(tx.logs, log => wrapLogBigNumbers(log)); - return tx; - } - public async getOrderHashAsync(signedOrder: SignedOrder): Promise<string> { - const shouldThrowOnInsufficientBalanceOrAllowance = false; - const params = signedOrderUtils.getOrderAddressesAndValues(signedOrder); - const orderHash = await this._exchange.getOrderHash.callAsync(params.orderAddresses, params.orderValues); - return orderHash; - } - public async isValidSignatureAsync(signedOrder: SignedOrder): Promise<boolean> { - const isValidSignature = await this._exchange.isValidSignature.callAsync( - signedOrder.maker, - ZeroEx.getOrderHashHex(signedOrder), - signedOrder.ecSignature.v, - signedOrder.ecSignature.r, - signedOrder.ecSignature.s, - ); - return isValidSignature; - } - public async isRoundingErrorAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise<boolean> { - const isRoundingError = await this._exchange.isRoundingError.callAsync(numerator, denominator, target); - return isRoundingError; - } - public async getPartialAmountAsync( - numerator: BigNumber, - denominator: BigNumber, - target: BigNumber, - ): Promise<BigNumber> { - const partialAmount = new BigNumber( - await this._exchange.getPartialAmount.callAsync(numerator, denominator, target), - ); - return partialAmount; - } -} - -function wrapLogBigNumbers(log: any): any { - const argNames = _.keys(log.args); - for (const argName of argNames) { - const isWeb3BigNumber = _.startsWith(log.args[argName].constructor.toString(), 'function BigNumber('); - if (isWeb3BigNumber) { - log.args[argName] = new BigNumber(log.args[argName]); - } - } -} diff --git a/packages/contracts/util/formatters.ts b/packages/contracts/util/formatters.ts deleted file mode 100644 index 8e0dfb09e..000000000 --- a/packages/contracts/util/formatters.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { SignedOrder } from '0x.js'; -import { BigNumber } from '@0xproject/utils'; -import * as _ from 'lodash'; - -import { BatchCancelOrders, BatchFillOrders, FillOrdersUpTo } from './types'; - -export const formatters = { - createBatchFill( - signedOrders: SignedOrder[], - shouldThrowOnInsufficientBalanceOrAllowance: boolean, - fillTakerTokenAmounts: BigNumber[] = [], - ) { - const batchFill: BatchFillOrders = { - orderAddresses: [], - orderValues: [], - fillTakerTokenAmounts, - shouldThrowOnInsufficientBalanceOrAllowance, - v: [], - r: [], - s: [], - }; - _.forEach(signedOrders, signedOrder => { - batchFill.orderAddresses.push([ - signedOrder.maker, - signedOrder.taker, - signedOrder.makerTokenAddress, - signedOrder.takerTokenAddress, - signedOrder.feeRecipient, - ]); - batchFill.orderValues.push([ - signedOrder.makerTokenAmount, - signedOrder.takerTokenAmount, - signedOrder.makerFee, - signedOrder.takerFee, - signedOrder.expirationUnixTimestampSec, - signedOrder.salt, - ]); - batchFill.v.push(signedOrder.ecSignature.v); - batchFill.r.push(signedOrder.ecSignature.r); - batchFill.s.push(signedOrder.ecSignature.s); - if (fillTakerTokenAmounts.length < signedOrders.length) { - batchFill.fillTakerTokenAmounts.push(signedOrder.takerTokenAmount); - } - }); - return batchFill; - }, - createFillUpTo( - signedOrders: SignedOrder[], - shouldThrowOnInsufficientBalanceOrAllowance: boolean, - fillTakerTokenAmount: BigNumber, - ) { - const fillUpTo: FillOrdersUpTo = { - orderAddresses: [], - orderValues: [], - fillTakerTokenAmount, - shouldThrowOnInsufficientBalanceOrAllowance, - v: [], - r: [], - s: [], - }; - signedOrders.forEach(signedOrder => { - fillUpTo.orderAddresses.push([ - signedOrder.maker, - signedOrder.taker, - signedOrder.makerTokenAddress, - signedOrder.takerTokenAddress, - signedOrder.feeRecipient, - ]); - fillUpTo.orderValues.push([ - signedOrder.makerTokenAmount, - signedOrder.takerTokenAmount, - signedOrder.makerFee, - signedOrder.takerFee, - signedOrder.expirationUnixTimestampSec, - signedOrder.salt, - ]); - fillUpTo.v.push(signedOrder.ecSignature.v); - fillUpTo.r.push(signedOrder.ecSignature.r); - fillUpTo.s.push(signedOrder.ecSignature.s); - }); - return fillUpTo; - }, - createBatchCancel(signedOrders: SignedOrder[], cancelTakerTokenAmounts: BigNumber[] = []) { - const batchCancel: BatchCancelOrders = { - orderAddresses: [], - orderValues: [], - cancelTakerTokenAmounts, - }; - signedOrders.forEach(signedOrder => { - batchCancel.orderAddresses.push([ - signedOrder.maker, - signedOrder.taker, - signedOrder.makerTokenAddress, - signedOrder.takerTokenAddress, - signedOrder.feeRecipient, - ]); - batchCancel.orderValues.push([ - signedOrder.makerTokenAmount, - signedOrder.takerTokenAmount, - signedOrder.makerFee, - signedOrder.takerFee, - signedOrder.expirationUnixTimestampSec, - signedOrder.salt, - ]); - if (cancelTakerTokenAmounts.length < signedOrders.length) { - batchCancel.cancelTakerTokenAmounts.push(signedOrder.takerTokenAmount); - } - }); - return batchCancel; - }, -}; diff --git a/packages/contracts/util/multi_sig_wrapper.ts b/packages/contracts/util/multi_sig_wrapper.ts deleted file mode 100644 index 9f6dcec52..000000000 --- a/packages/contracts/util/multi_sig_wrapper.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { AbiDefinition, MethodAbi } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import ABI = require('ethereumjs-abi'); -import ethUtil = require('ethereumjs-util'); -import * as _ from 'lodash'; -import * as Web3 from 'web3'; - -import { MultiSigWalletContract } from '../src/contract_wrappers/generated/multi_sig_wallet'; - -import { TransactionDataParams } from './types'; - -export class MultiSigWrapper { - private _multiSig: MultiSigWalletContract; - public static encodeFnArgs(name: string, abi: AbiDefinition[], args: any[]) { - const abiEntity = _.find(abi, { name }) as MethodAbi; - if (_.isUndefined(abiEntity)) { - throw new Error(`Did not find abi entry for name: ${name}`); - } - const types = _.map(abiEntity.inputs, input => input.type); - const funcSig = ethUtil.bufferToHex(ABI.methodID(name, types)); - const argsData = _.map(args, arg => { - const target = _.isBoolean(arg) ? +arg : arg; - const targetBuff = ethUtil.toBuffer(target); - return ethUtil.setLengthLeft(targetBuff, 32).toString('hex'); - }); - return funcSig + argsData.join(''); - } - constructor(multiSigContract: MultiSigWalletContract) { - this._multiSig = multiSigContract; - } - public async submitTransactionAsync( - destination: string, - from: string, - dataParams: TransactionDataParams, - value: BigNumber = new BigNumber(0), - ) { - const { name, abi, args = [] } = dataParams; - const encoded = MultiSigWrapper.encodeFnArgs(name, abi, args); - return this._multiSig.submitTransaction.sendTransactionAsync(destination, value, encoded, { - from, - }); - } -} diff --git a/packages/contracts/util/order_factory.ts b/packages/contracts/util/order_factory.ts deleted file mode 100644 index 8ba5df24a..000000000 --- a/packages/contracts/util/order_factory.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Order, SignedOrder, ZeroEx } from '0x.js'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as _ from 'lodash'; - -import { DefaultOrderParams } from './types'; - -export class OrderFactory { - private _defaultOrderParams: Partial<Order>; - private _zeroEx: ZeroEx; - constructor(zeroEx: ZeroEx, defaultOrderParams: Partial<Order>) { - this._defaultOrderParams = defaultOrderParams; - this._zeroEx = zeroEx; - } - public async newSignedOrderAsync(customOrderParams: Partial<Order> = {}): Promise<SignedOrder> { - const randomExpiration = new BigNumber(Math.floor((Date.now() + Math.random() * 100000000000) / 1000)); - const order = ({ - expirationUnixTimestampSec: randomExpiration, - salt: ZeroEx.generatePseudoRandomSalt(), - taker: ZeroEx.NULL_ADDRESS, - ...this._defaultOrderParams, - ...customOrderParams, - } as any) as Order; - const orderHashHex = ZeroEx.getOrderHashHex(order); - const shouldAddPersonalMessagePrefix = false; - const ecSignature = await this._zeroEx.signOrderHashAsync( - orderHashHex, - order.maker, - shouldAddPersonalMessagePrefix, - ); - const signedOrder = { - ...order, - ecSignature, - }; - return signedOrder; - } -} diff --git a/packages/contracts/util/signed_order_utils.ts b/packages/contracts/util/signed_order_utils.ts deleted file mode 100644 index fc2f800cd..000000000 --- a/packages/contracts/util/signed_order_utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { SignedOrder } from '0x.js'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import ethUtil = require('ethereumjs-util'); -import * as _ from 'lodash'; - -import { crypto } from './crypto'; - -export const signedOrderUtils = { - createFill: ( - signedOrder: SignedOrder, - shouldThrowOnInsufficientBalanceOrAllowance?: boolean, - fillTakerTokenAmount?: BigNumber, - ) => { - const fill = { - ...signedOrderUtils.getOrderAddressesAndValues(signedOrder), - fillTakerTokenAmount: fillTakerTokenAmount || signedOrder.takerTokenAmount, - shouldThrowOnInsufficientBalanceOrAllowance: !!shouldThrowOnInsufficientBalanceOrAllowance, - ...signedOrder.ecSignature, - }; - return fill; - }, - createCancel(signedOrder: SignedOrder, cancelTakerTokenAmount?: BigNumber) { - const cancel = { - ...signedOrderUtils.getOrderAddressesAndValues(signedOrder), - cancelTakerTokenAmount: cancelTakerTokenAmount || signedOrder.takerTokenAmount, - }; - return cancel; - }, - getOrderAddressesAndValues(signedOrder: SignedOrder) { - return { - orderAddresses: [ - signedOrder.maker, - signedOrder.taker, - signedOrder.makerTokenAddress, - signedOrder.takerTokenAddress, - signedOrder.feeRecipient, - ], - orderValues: [ - signedOrder.makerTokenAmount, - signedOrder.takerTokenAmount, - signedOrder.makerFee, - signedOrder.takerFee, - signedOrder.expirationUnixTimestampSec, - signedOrder.salt, - ], - }; - }, -}; diff --git a/packages/contracts/util/types.ts b/packages/contracts/util/types.ts deleted file mode 100644 index 321084c42..000000000 --- a/packages/contracts/util/types.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { AbiDefinition, ContractAbi } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; - -export interface BalancesByOwner { - [ownerAddress: string]: { - [tokenAddress: string]: BigNumber; - }; -} - -export interface SubmissionContractEventArgs { - transactionId: BigNumber; -} - -export interface BatchFillOrders { - orderAddresses: string[][]; - orderValues: BigNumber[][]; - fillTakerTokenAmounts: BigNumber[]; - shouldThrowOnInsufficientBalanceOrAllowance: boolean; - v: number[]; - r: string[]; - s: string[]; -} - -export interface FillOrdersUpTo { - orderAddresses: string[][]; - orderValues: BigNumber[][]; - fillTakerTokenAmount: BigNumber; - shouldThrowOnInsufficientBalanceOrAllowance: boolean; - v: number[]; - r: string[]; - s: string[]; -} - -export interface BatchCancelOrders { - orderAddresses: string[][]; - orderValues: BigNumber[][]; - cancelTakerTokenAmounts: BigNumber[]; -} - -export interface DefaultOrderParams { - exchangeContractAddress: string; - maker: string; - feeRecipient: string; - makerTokenAddress: string; - takerTokenAddress: string; - makerTokenAmount: BigNumber; - takerTokenAmount: BigNumber; - makerFee: BigNumber; - takerFee: BigNumber; -} - -export interface TransactionDataParams { - name: string; - abi: AbiDefinition[]; - args: any[]; -} - -export interface MultiSigConfig { - owners: string[]; - confirmationsRequired: number; - secondsRequired: number; -} - -export interface MultiSigConfigByNetwork { - [networkName: string]: MultiSigConfig; -} - -export interface Token { - address?: string; - name: string; - symbol: string; - decimals: number; - ipfsHash: string; - swarmHash: string; -} - -export interface TokenInfoByNetwork { - development: Token[]; - live: Token[]; -} - -export enum ExchangeContractErrs { - ERROR_ORDER_EXPIRED, - ERROR_ORDER_FULLY_FILLED_OR_CANCELLED, - ERROR_ROUNDING_ERROR_TOO_LARGE, - ERROR_INSUFFICIENT_BALANCE_OR_ALLOWANCE, -} - -export enum ContractName { - TokenTransferProxy = 'TokenTransferProxy', - TokenRegistry = 'TokenRegistry', - MultiSigWalletWithTimeLock = 'MultiSigWalletWithTimeLock', - Exchange = 'Exchange', - ZRXToken = 'ZRXToken', - DummyToken = 'DummyToken', - EtherToken = 'WETH9', - MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress = 'MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress', - MaliciousToken = 'MaliciousToken', - AccountLevels = 'AccountLevels', - EtherDelta = 'EtherDelta', - Arbitrage = 'Arbitrage', -} - -export interface Artifact { - contract_name: ContractName; - networks: { - [networkId: number]: { - abi: ContractAbi; - solc_version: string; - keccak256: string; - optimizer_enabled: number; - unlinked_binary: string; - updated_at: number; - address: string; - constructor_args: string; - }; - }; -} |