diff options
19 files changed, 1583 insertions, 241 deletions
diff --git a/packages/contracts/CHANGELOG.json b/packages/contracts/CHANGELOG.json index 00f94c83b..7dfa06990 100644 --- a/packages/contracts/CHANGELOG.json +++ b/packages/contracts/CHANGELOG.json @@ -1,5 +1,15 @@ [ { + "name": "MultiAssetProxy", + "version": "1.0.0", + "changes": [ + { + "note": "Add MultiAssetProxy implementation", + "pr": 1224 + } + ] + }, + { "name": "OrderValidator", "version": "1.0.1", "changes": [ diff --git a/packages/contracts/compiler.json b/packages/contracts/compiler.json index af3980b4e..c824e4645 100644 --- a/packages/contracts/compiler.json +++ b/packages/contracts/compiler.json @@ -38,6 +38,7 @@ "IValidator", "IWallet", "MixinAuthorizable", + "MultiAssetProxy", "MultiSigWallet", "MultiSigWalletWithTimeLock", "OrderValidator", diff --git a/packages/contracts/contracts/protocol/AssetProxy/MultiAssetProxy.sol b/packages/contracts/contracts/protocol/AssetProxy/MultiAssetProxy.sol new file mode 100644 index 000000000..42231e73b --- /dev/null +++ b/packages/contracts/contracts/protocol/AssetProxy/MultiAssetProxy.sol @@ -0,0 +1,300 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + +import "../Exchange/MixinAssetProxyDispatcher.sol"; +import "./MixinAuthorizable.sol"; + + +contract MultiAssetProxy is + MixinAssetProxyDispatcher, + MixinAuthorizable +{ + // Id of this proxy. + bytes4 constant internal PROXY_ID = bytes4(keccak256("MultiAsset(uint256[],bytes[])")); + + // solhint-disable-next-line payable-fallback + function () + external + { + assembly { + // The first 4 bytes of calldata holds the function selector + let selector := and(calldataload(0), 0xffffffff00000000000000000000000000000000000000000000000000000000) + + // `transferFrom` will be called with the following parameters: + // assetData Encoded byte array. + // from Address to transfer asset from. + // to Address to transfer asset to. + // amount Amount of asset to transfer. + // bytes4(keccak256("transferFrom(bytes,address,address,uint256)")) = 0xa85e59e4 + if eq(selector, 0xa85e59e400000000000000000000000000000000000000000000000000000000) { + + // To lookup a value in a mapping, we load from the storage location keccak256(k, p), + // where k is the key left padded to 32 bytes and p is the storage slot + mstore(0, caller) + mstore(32, authorized_slot) + + // Revert if authorized[msg.sender] == false + if iszero(sload(keccak256(0, 64))) { + // Revert with `Error("SENDER_NOT_AUTHORIZED")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000001553454e4445525f4e4f545f415554484f52495a454400000000000000) + mstore(96, 0) + revert(0, 100) + } + + // `transferFrom`. + // The function is marked `external`, so no abi decoding is done for + // us. Instead, we expect the `calldata` memory to contain the + // following: + // + // | Area | Offset | Length | Contents | + // |----------|--------|---------|-------------------------------------| + // | Header | 0 | 4 | function selector | + // | Params | | 4 * 32 | function parameters: | + // | | 4 | | 1. offset to assetData (*) | + // | | 36 | | 2. from | + // | | 68 | | 3. to | + // | | 100 | | 4. amount | + // | Data | | | assetData: | + // | | 132 | 32 | assetData Length | + // | | 164 | ** | assetData Contents | + // + // (*): offset is computed from start of function parameters, so offset + // by an additional 4 bytes in the calldata. + // + // (**): see table below to compute length of assetData Contents + // + // WARNING: The ABIv2 specification allows additional padding between + // the Params and Data section. This will result in a larger + // offset to assetData. + + // Load offset to `assetData` + let assetDataOffset := calldataload(4) + + // Asset data itself is encoded as follows: + // + // | Area | Offset | Length | Contents | + // |----------|-------------|---------|-------------------------------------| + // | Header | 0 | 4 | assetProxyId | + // | Params | | 2 * 32 | function parameters: | + // | | 4 | | 1. offset to amounts (*) | + // | | 36 | | 2. offset to nestedAssetData (*) | + // | Data | | | amounts: | + // | | 68 | 32 | amounts Length | + // | | 100 | a | amounts Contents | + // | | | | nestedAssetData: | + // | | 100 + a | 32 | nestedAssetData Length | + // | | 132 + a | b | nestedAssetData Contents (offsets) | + // | | 132 + a + b | | nestedAssetData[0, ..., len] | + + // In order to find the offset to `amounts`, we must add: + // 4 (function selector) + // + assetDataOffset + // + 32 (assetData len) + // + 4 (assetProxyId) + let amountsOffset := calldataload(add(assetDataOffset, 40)) + + // In order to find the offset to `nestedAssetData`, we must add: + // 4 (function selector) + // + assetDataOffset + // + 32 (assetData len) + // + 4 (assetProxyId) + // + 32 (amounts offset) + let nestedAssetDataOffset := calldataload(add(assetDataOffset, 72)) + + // In order to find the start of the `amounts` contents, we must add: + // 4 (function selector) + // + assetDataOffset + // + 32 (assetData len) + // + 4 (assetProxyId) + // + amountsOffset + // + 32 (amounts len) + let amountsContentsStart := add(assetDataOffset, add(amountsOffset, 72)) + + // Load number of elements in `amounts` + let amountsLen := calldataload(sub(amountsContentsStart, 32)) + + // In order to find the start of the `nestedAssetData` contents, we must add: + // 4 (function selector) + // + assetDataOffset + // + 32 (assetData len) + // + 4 (assetProxyId) + // + nestedAssetDataOffset + // + 32 (nestedAssetData len) + let nestedAssetDataContentsStart := add(assetDataOffset, add(nestedAssetDataOffset, 72)) + + // Load number of elements in `nestedAssetData` + let nestedAssetDataLen := calldataload(sub(nestedAssetDataContentsStart, 32)) + + // Revert if number of elements in `amounts` differs from number of elements in `nestedAssetData` + if iszero(eq(amountsLen, nestedAssetDataLen)) { + // Revert with `Error("LENGTH_MISMATCH")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000000f4c454e4754485f4d49534d4154434800000000000000000000000000) + mstore(96, 0) + revert(0, 100) + } + + // Copy `transferFrom` selector, offset to `assetData`, `from`, and `to` from calldata to memory + calldatacopy( + 0, // memory can safely be overwritten from beginning + 0, // start of calldata + 100 // length of selector (4) and 3 params (32 * 3) + ) + + // Overwrite existing offset to `assetData` with our own + mstore(4, 128) + + // Load `amount` + let amount := calldataload(100) + + // Calculate number of bytes in `amounts` contents + let amountsByteLen := mul(amountsLen, 32) + + // Initialize `assetProxyId` and `assetProxy` to 0 + let assetProxyId := 0 + let assetProxy := 0 + + // Loop through `amounts` and `nestedAssetData`, calling `transferFrom` for each respective element + for {let i := 0} lt(i, amountsByteLen) {i := add(i, 32)} { + + // Calculate the total amount + let amountsElement := calldataload(add(amountsContentsStart, i)) + let totalAmount := mul(amountsElement, amount) + + // Revert if multiplication resulted in an overflow + if iszero(eq(div(totalAmount, amount), amountsElement)) { + // Revert with `Error("UINT256_OVERFLOW")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000001055494e543235365f4f564552464c4f57000000000000000000000000) + mstore(96, 0) + revert(0, 100) + } + + // Write `totalAmount` to memory + mstore(100, totalAmount) + + // Load offset to `nestedAssetData[i]` + let nestedAssetDataElementOffset := calldataload(add(nestedAssetDataContentsStart, i)) + + // In order to find the start of the `nestedAssetData[i]` contents, we must add: + // 4 (function selector) + // + assetDataOffset + // + 32 (assetData len) + // + 4 (assetProxyId) + // + nestedAssetDataOffset + // + 32 (nestedAssetData len) + // + nestedAssetDataElementOffset + // + 32 (nestedAssetDataElement len) + let nestedAssetDataElementContentsStart := add(assetDataOffset, add(nestedAssetDataOffset, add(nestedAssetDataElementOffset, 104))) + + // Load length of `nestedAssetData[i]` + let nestedAssetDataElementLenStart := sub(nestedAssetDataElementContentsStart, 32) + let nestedAssetDataElementLen := calldataload(nestedAssetDataElementLenStart) + + // Revert if the `nestedAssetData` does not contain a 4 byte `assetProxyId` + if lt(nestedAssetDataElementLen, 4) { + // Revert with `Error("LENGTH_GREATER_THAN_3_REQUIRED")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000001e4c454e4754485f475245415445525f5448414e5f335f524551554952) + mstore(96, 0x4544000000000000000000000000000000000000000000000000000000000000) + revert(0, 100) + } + + // Load AssetProxy id + let currentAssetProxyId := and( + calldataload(nestedAssetDataElementContentsStart), + 0xffffffff00000000000000000000000000000000000000000000000000000000 + ) + + // Only load `assetProxy` if `currentAssetProxyId` does not equal `assetProxyId` + // We do not need to check if `currentAssetProxyId` is 0 since `assetProxy` is also initialized to 0 + if iszero(eq(currentAssetProxyId, assetProxyId)) { + // Update `assetProxyId` + assetProxyId := currentAssetProxyId + // To lookup a value in a mapping, we load from the storage location keccak256(k, p), + // where k is the key left padded to 32 bytes and p is the storage slot + mstore(132, assetProxyId) + mstore(164, assetProxies_slot) + assetProxy := sload(keccak256(132, 64)) + } + + // Revert if AssetProxy with given id does not exist + if iszero(assetProxy) { + // Revert with `Error("ASSET_PROXY_DOES_NOT_EXIST")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000001a41535345545f50524f58595f444f45535f4e4f545f45584953540000) + mstore(96, 0) + revert(0, 100) + } + + // Copy `nestedAssetData[i]` from calldata to memory + calldatacopy( + 132, // memory slot after `amounts[i]` + nestedAssetDataElementLenStart, // location of `nestedAssetData[i]` in calldata + add(nestedAssetDataElementLen, 32) // `nestedAssetData[i].length` plus 32 byte length + ) + + // call `assetProxy.transferFrom` + let success := call( + gas, // forward all gas + assetProxy, // call address of asset proxy + 0, // don't send any ETH + 0, // pointer to start of input + add(164, nestedAssetDataElementLen), // length of input + 0, // write output over memory that won't be reused + 0 // don't copy output to memory + ) + + // Revert with reason given by AssetProxy if `transferFrom` call failed + if iszero(success) { + returndatacopy( + 0, // copy to memory at 0 + 0, // copy from return data at 0 + returndatasize() // copy all return data + ) + revert(0, returndatasize()) + } + } + + // Return if no `transferFrom` calls reverted + return(0, 0) + } + + // Revert if undefined function is called + revert(0, 0) + } + } + + /// @dev Gets the proxy id associated with the proxy address. + /// @return Proxy id. + function getProxyId() + external + pure + returns (bytes4) + { + return PROXY_ID; + } +} diff --git a/packages/contracts/contracts/protocol/AssetProxy/interfaces/IAssetData.sol b/packages/contracts/contracts/protocol/AssetProxy/interfaces/IAssetData.sol index 3e76e38dd..e2da68919 100644 --- a/packages/contracts/contracts/protocol/AssetProxy/interfaces/IAssetData.sol +++ b/packages/contracts/contracts/protocol/AssetProxy/interfaces/IAssetData.sol @@ -18,6 +18,7 @@ // solhint-disable pragma solidity 0.4.24; +pragma experimental ABIEncoderV2; // @dev Interface of the asset proxy's assetData. @@ -26,15 +27,18 @@ pragma solidity 0.4.24; interface IAssetData { function ERC20Token(address tokenContract) - external - pure; + external; function ERC721Token( address tokenContract, - uint256 tokenId, - bytes receiverData + uint256 tokenId ) - external - pure; + external; + + function MultiAsset( + uint256[] amounts, + bytes[] nestedAssetData + ) + external; } diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 6f031fcee..16dd3c9c1 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -19,7 +19,8 @@ "test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov", "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html", "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha", - "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", + "run_mocha": + "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", "compile": "sol-compiler --contracts-dir contracts", "clean": "shx rm -rf lib generated-artifacts generated-wrappers", "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers", @@ -32,7 +33,8 @@ "lint-contracts": "solhint contracts/**/**/**/**/*.sol" }, "config": { - "abis": "generated-artifacts/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyMultipleReturnERC20Token|DummyNoReturnERC20Token|ERC20Token|ERC20Proxy|ERC721Token|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|InvalidERC721Receiver|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|OrderValidator|ReentrantERC20Token|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|TestStaticCallReceiver|Validator|Wallet|Whitelist|WETH9|ZRXToken).json" + "abis": + "generated-artifacts/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyMultipleReturnERC20Token|DummyNoReturnERC20Token|ERC20Token|ERC20Proxy|ERC721Token|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|InvalidERC721Receiver|MixinAuthorizable|MultiAssetProxy|MultiSigWallet|MultiSigWalletWithTimeLock|OrderValidator|ReentrantERC20Token|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|TestStaticCallReceiver|Validator|Wallet|Whitelist|WETH9|ZRXToken).json" }, "repository": { "type": "git", diff --git a/packages/contracts/src/artifacts/index.ts b/packages/contracts/src/artifacts/index.ts index c30972a91..97c1b6209 100644 --- a/packages/contracts/src/artifacts/index.ts +++ b/packages/contracts/src/artifacts/index.ts @@ -19,6 +19,7 @@ import * as InvalidERC721Receiver from '../../generated-artifacts/InvalidERC721R import * as IValidator from '../../generated-artifacts/IValidator.json'; import * as IWallet from '../../generated-artifacts/IWallet.json'; import * as MixinAuthorizable from '../../generated-artifacts/MixinAuthorizable.json'; +import * as MultiAssetProxy from '../../generated-artifacts/MultiAssetProxy.json'; import * as MultiSigWallet from '../../generated-artifacts/MultiSigWallet.json'; import * as MultiSigWalletWithTimeLock from '../../generated-artifacts/MultiSigWalletWithTimeLock.json'; import * as OrderValidator from '../../generated-artifacts/OrderValidator.json'; @@ -57,6 +58,7 @@ export const artifacts = { IWallet: IWallet as ContractArtifact, InvalidERC721Receiver: InvalidERC721Receiver as ContractArtifact, MixinAuthorizable: MixinAuthorizable as ContractArtifact, + MultiAssetProxy: MultiAssetProxy as ContractArtifact, MultiSigWallet: MultiSigWallet as ContractArtifact, MultiSigWalletWithTimeLock: MultiSigWalletWithTimeLock as ContractArtifact, OrderValidator: OrderValidator as ContractArtifact, diff --git a/packages/contracts/test/asset_proxy/proxies.ts b/packages/contracts/test/asset_proxy/proxies.ts index b8305993e..8fa1e602a 100644 --- a/packages/contracts/test/asset_proxy/proxies.ts +++ b/packages/contracts/test/asset_proxy/proxies.ts @@ -12,7 +12,9 @@ import { DummyMultipleReturnERC20TokenContract } from '../../generated-wrappers/ import { DummyNoReturnERC20TokenContract } from '../../generated-wrappers/dummy_no_return_erc20_token'; import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy'; import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy'; +import { IAssetDataContract } from '../../generated-wrappers/i_asset_data'; import { IAssetProxyContract } from '../../generated-wrappers/i_asset_proxy'; +import { MultiAssetProxyContract } from '../../generated-wrappers/multi_asset_proxy'; import { artifacts } from '../../src/artifacts'; import { expectTransactionFailedAsync, expectTransactionFailedWithoutReasonAsync } from '../utils/assertions'; import { chaiSetup } from '../utils/chai_setup'; @@ -30,26 +32,35 @@ const assetProxyInterface = new IAssetProxyContract( constants.NULL_ADDRESS, provider, ); +const assetDataInterface = new IAssetDataContract( + artifacts.IAssetData.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 authorized: string; + let fromAddress: string; + let toAddress: string; - let zrxToken: DummyERC20TokenContract; - let erc721Token: DummyERC721TokenContract; + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let erc721TokenA: DummyERC721TokenContract; + let erc721TokenB: DummyERC721TokenContract; let erc721Receiver: DummyERC721ReceiverContract; let erc20Proxy: ERC20ProxyContract; let erc721Proxy: ERC721ProxyContract; let noReturnErc20Token: DummyNoReturnERC20TokenContract; let multipleReturnErc20Token: DummyMultipleReturnERC20TokenContract; + let multiAssetProxy: MultiAssetProxyContract; let erc20Wrapper: ERC20Wrapper; let erc721Wrapper: ERC721Wrapper; - let erc721MakerTokenId: BigNumber; + let erc721AFromTokenId: BigNumber; + let erc721BFromTokenId: BigNumber; before(async () => { await blockchainLifecycle.startAsync(); @@ -59,41 +70,73 @@ describe('Asset Transfer Proxies', () => { }); before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); - const usedAddresses = ([owner, notAuthorized, exchangeAddress, makerAddress, takerAddress] = _.slice( - accounts, - 0, - 5, - )); + const usedAddresses = ([owner, notAuthorized, authorized, fromAddress, toAddress] = _.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); + // Deploy AssetProxies erc20Proxy = await erc20Wrapper.deployProxyAsync(); - await erc20Wrapper.setBalancesAndAllowancesAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync( + artifacts.MultiAssetProxy, + provider, + txDefaults, + ); + + // Configure ERC20Proxy await web3Wrapper.awaitTransactionSuccessAsync( - await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeAddress, { + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(authorized, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, { from: owner, }), constants.AWAIT_TRANSACTION_MINED_MS, ); - [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); - erc721Proxy = await erc721Wrapper.deployProxyAsync(); - await erc721Wrapper.setBalancesAndAllowancesAsync(); - const erc721Balances = await erc721Wrapper.getBalancesAsync(); - erc721MakerTokenId = erc721Balances[makerAddress][erc721Token.address][0]; + // Configure ERC721Proxy await web3Wrapper.awaitTransactionSuccessAsync( - await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeAddress, { + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(authorized, { from: owner, }), constants.AWAIT_TRANSACTION_MINED_MS, ); - erc721Receiver = await DummyERC721ReceiverContract.deployFrom0xArtifactAsync( - artifacts.DummyERC721Receiver, - provider, - txDefaults, + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + // Configure MultiAssetProxy + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.addAuthorizedAddress.sendTransactionAsync(authorized, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc721Proxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + // Deploy and configure ERC20 tokens + const numDummyErc20ToDeploy = 2; + [erc20TokenA, erc20TokenB] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, ); noReturnErc20Token = await DummyNoReturnERC20TokenContract.deployFrom0xArtifactAsync( artifacts.DummyNoReturnERC20Token, @@ -104,30 +147,32 @@ describe('Asset Transfer Proxies', () => { constants.DUMMY_TOKEN_DECIMALS, constants.DUMMY_TOKEN_TOTAL_SUPPLY, ); + multipleReturnErc20Token = await DummyMultipleReturnERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyMultipleReturnERC20Token, + provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + constants.DUMMY_TOKEN_DECIMALS, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ); + + await erc20Wrapper.setBalancesAndAllowancesAsync(); await web3Wrapper.awaitTransactionSuccessAsync( - await noReturnErc20Token.setBalance.sendTransactionAsync(makerAddress, constants.INITIAL_ERC20_BALANCE), + await noReturnErc20Token.setBalance.sendTransactionAsync(fromAddress, constants.INITIAL_ERC20_BALANCE), constants.AWAIT_TRANSACTION_MINED_MS, ); await web3Wrapper.awaitTransactionSuccessAsync( await noReturnErc20Token.approve.sendTransactionAsync( erc20Proxy.address, constants.INITIAL_ERC20_ALLOWANCE, - { from: makerAddress }, + { from: fromAddress }, ), constants.AWAIT_TRANSACTION_MINED_MS, ); - multipleReturnErc20Token = await DummyMultipleReturnERC20TokenContract.deployFrom0xArtifactAsync( - artifacts.DummyMultipleReturnERC20Token, - provider, - txDefaults, - constants.DUMMY_TOKEN_NAME, - constants.DUMMY_TOKEN_SYMBOL, - constants.DUMMY_TOKEN_DECIMALS, - constants.DUMMY_TOKEN_TOTAL_SUPPLY, - ); await web3Wrapper.awaitTransactionSuccessAsync( await multipleReturnErc20Token.setBalance.sendTransactionAsync( - makerAddress, + fromAddress, constants.INITIAL_ERC20_BALANCE, ), constants.AWAIT_TRANSACTION_MINED_MS, @@ -136,10 +181,23 @@ describe('Asset Transfer Proxies', () => { await multipleReturnErc20Token.approve.sendTransactionAsync( erc20Proxy.address, constants.INITIAL_ERC20_ALLOWANCE, - { from: makerAddress }, + { from: fromAddress }, ), constants.AWAIT_TRANSACTION_MINED_MS, ); + + // Deploy and configure ERC721 tokens and receiver + [erc721TokenA, erc721TokenB] = await erc721Wrapper.deployDummyTokensAsync(); + erc721Receiver = await DummyERC721ReceiverContract.deployFrom0xArtifactAsync( + artifacts.DummyERC721Receiver, + provider, + txDefaults, + ); + + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721AFromTokenId = erc721Balances[fromAddress][erc721TokenA.address][0]; + erc721BFromTokenId = erc721Balances[fromAddress][erc721TokenB.address][0]; }); beforeEach(async () => { await blockchainLifecycle.startAsync(); @@ -147,7 +205,8 @@ describe('Asset Transfer Proxies', () => { afterEach(async () => { await blockchainLifecycle.revertAsync(); }); - describe('Transfer Proxy - ERC20', () => { + + describe('ERC20Proxy', () => { it('should revert if undefined function is called', async () => { const undefinedSelector = '0x01020304'; await expectTransactionFailedWithoutReasonAsync( @@ -159,141 +218,146 @@ describe('Asset Transfer Proxies', () => { }), ); }); + it('should have an id of 0xf47261b0', async () => { + const proxyId = await erc20Proxy.getProxyId.callAsync(); + const expectedProxyId = '0xf47261b0'; + expect(proxyId).to.equal(expectedProxyId); + }); describe('transferFrom', () => { it('should successfully transfer tokens', async () => { // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - // Perform a transfer from makerAddress to takerAddress + const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + // Perform a transfer from fromAddress to toAddress const erc20Balances = await erc20Wrapper.getBalancesAsync(); const amount = new BigNumber(10); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), 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[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(amount), ); - expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][zrxToken.address].add(amount), + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(amount), ); }); it('should successfully transfer tokens that do not return a value', async () => { // Construct ERC20 asset data const encodedAssetData = assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address); - // Perform a transfer from makerAddress to takerAddress - const initialMakerBalance = await noReturnErc20Token.balanceOf.callAsync(makerAddress); - const initialTakerBalance = await noReturnErc20Token.balanceOf.callAsync(takerAddress); + // Perform a transfer from fromAddress to toAddress + const initialFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress); + const initialToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress); const amount = new BigNumber(10); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), constants.AWAIT_TRANSACTION_MINED_MS, ); // Verify transfer was successful - const newMakerBalance = await noReturnErc20Token.balanceOf.callAsync(makerAddress); - const newTakerBalance = await noReturnErc20Token.balanceOf.callAsync(takerAddress); - expect(newMakerBalance).to.be.bignumber.equal(initialMakerBalance.minus(amount)); - expect(newTakerBalance).to.be.bignumber.equal(initialTakerBalance.plus(amount)); + const newFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress); + const newToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress); + expect(newFromBalance).to.be.bignumber.equal(initialFromBalance.minus(amount)); + expect(newToBalance).to.be.bignumber.equal(initialToBalance.plus(amount)); }); it('should successfully transfer tokens and ignore extra assetData', async () => { // Construct ERC20 asset data const extraData = '0102030405060708'; - const encodedAssetData = `${assetDataUtils.encodeERC20AssetData(zrxToken.address)}${extraData}`; - // Perform a transfer from makerAddress to takerAddress + const encodedAssetData = `${assetDataUtils.encodeERC20AssetData(erc20TokenA.address)}${extraData}`; + // Perform a transfer from fromAddress to toAddress const erc20Balances = await erc20Wrapper.getBalancesAsync(); const amount = new BigNumber(10); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), 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[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(amount), ); - expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][zrxToken.address].add(amount), + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(amount), ); }); it('should do nothing if transferring 0 amount of a token', async () => { // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - // Perform a transfer from makerAddress to takerAddress + const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + // Perform a transfer from fromAddress to toAddress const erc20Balances = await erc20Wrapper.getBalancesAsync(); const amount = new BigNumber(0); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), 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[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address], ); - expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][zrxToken.address], + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address], ); }); - it('should throw if allowances are too low', async () => { + it('should revert if allowances are too low', async () => { // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); // Create allowance less than transfer amount. Set allowance on proxy. const allowance = new BigNumber(0); const amount = new BigNumber(10); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( - await zrxToken.approve.sendTransactionAsync(erc20Proxy.address, allowance, { - from: makerAddress, + await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, allowance, { + from: fromAddress, }), constants.AWAIT_TRANSACTION_MINED_MS, ); @@ -303,7 +367,7 @@ describe('Asset Transfer Proxies', () => { web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), RevertReason.TransferFailed, ); @@ -311,7 +375,7 @@ describe('Asset Transfer Proxies', () => { expect(newBalances).to.deep.equal(erc20Balances); }); - it('should throw if allowances are too low and token does not return a value', async () => { + it('should revert if allowances are too low and token does not return a value', async () => { // Construct ERC20 asset data const encodedAssetData = assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address); // Create allowance less than transfer amount. Set allowance on proxy. @@ -319,42 +383,42 @@ describe('Asset Transfer Proxies', () => { const amount = new BigNumber(10); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await noReturnErc20Token.approve.sendTransactionAsync(erc20Proxy.address, allowance, { - from: makerAddress, + from: fromAddress, }), constants.AWAIT_TRANSACTION_MINED_MS, ); - const initialMakerBalance = await noReturnErc20Token.balanceOf.callAsync(makerAddress); - const initialTakerBalance = await noReturnErc20Token.balanceOf.callAsync(takerAddress); + const initialFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress); + const initialToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress); // Perform a transfer; expect this to fail. await expectTransactionFailedAsync( web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), RevertReason.TransferFailed, ); - const newMakerBalance = await noReturnErc20Token.balanceOf.callAsync(makerAddress); - const newTakerBalance = await noReturnErc20Token.balanceOf.callAsync(takerAddress); - expect(newMakerBalance).to.be.bignumber.equal(initialMakerBalance); - expect(newTakerBalance).to.be.bignumber.equal(initialTakerBalance); + const newFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress); + const newToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress); + expect(newFromBalance).to.be.bignumber.equal(initialFromBalance); + expect(newToBalance).to.be.bignumber.equal(initialToBalance); }); - it('should throw if requesting address is not authorized', async () => { + it('should revert if caller is not authorized', async () => { // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - // Perform a transfer from makerAddress to takerAddress + const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + // Perform a transfer from fromAddress to toAddress const amount = new BigNumber(10); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); const erc20Balances = await erc20Wrapper.getBalancesAsync(); @@ -370,42 +434,36 @@ describe('Asset Transfer Proxies', () => { expect(newBalances).to.deep.equal(erc20Balances); }); - it('should throw if token returns more than 32 bytes', async () => { + it('should revert if token returns more than 32 bytes', async () => { // Construct ERC20 asset data const encodedAssetData = assetDataUtils.encodeERC20AssetData(multipleReturnErc20Token.address); const amount = new BigNumber(10); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); - const initialMakerBalance = await multipleReturnErc20Token.balanceOf.callAsync(makerAddress); - const initialTakerBalance = await multipleReturnErc20Token.balanceOf.callAsync(takerAddress); + const initialFromBalance = await multipleReturnErc20Token.balanceOf.callAsync(fromAddress); + const initialToBalance = await multipleReturnErc20Token.balanceOf.callAsync(toAddress); // Perform a transfer; expect this to fail. await expectTransactionFailedAsync( web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), RevertReason.TransferFailed, ); - const newMakerBalance = await multipleReturnErc20Token.balanceOf.callAsync(makerAddress); - const newTakerBalance = await multipleReturnErc20Token.balanceOf.callAsync(takerAddress); - expect(newMakerBalance).to.be.bignumber.equal(initialMakerBalance); - expect(newTakerBalance).to.be.bignumber.equal(initialTakerBalance); + const newFromBalance = await multipleReturnErc20Token.balanceOf.callAsync(fromAddress); + const newToBalance = await multipleReturnErc20Token.balanceOf.callAsync(toAddress); + expect(newFromBalance).to.be.bignumber.equal(initialFromBalance); + expect(newToBalance).to.be.bignumber.equal(initialToBalance); }); }); - - 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('ERC721Proxy', () => { it('should revert if undefined function is called', async () => { const undefinedSelector = '0x01020304'; await expectTransactionFailedWithoutReasonAsync( @@ -417,76 +475,81 @@ describe('Asset Transfer Proxies', () => { }), ); }); + it('should have an id of 0x02571792', async () => { + const proxyId = await erc721Proxy.getProxyId.callAsync(); + const expectedProxyId = '0x02571792'; + expect(proxyId).to.equal(expectedProxyId); + }); describe('transferFrom', () => { it('should successfully transfer tokens', async () => { // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress const amount = new BigNumber(1); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + from: authorized, }), constants.AWAIT_TRANSACTION_MINED_MS, ); // Verify transfer was successful - const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress); + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.bignumber.equal(toAddress); }); it('should successfully transfer tokens and ignore extra assetData', async () => { // Construct ERC721 asset data const extraData = '0102030405060708'; const encodedAssetData = `${assetDataUtils.encodeERC721AssetData( - erc721Token.address, - erc721MakerTokenId, + erc721TokenA.address, + erc721AFromTokenId, )}${extraData}`; // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress const amount = new BigNumber(1); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + from: authorized, }), constants.AWAIT_TRANSACTION_MINED_MS, ); // Verify transfer was successful - const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress); + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.bignumber.equal(toAddress); }); it('should not call onERC721Received when transferring to a smart contract', async () => { // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress const amount = new BigNumber(1); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, + fromAddress, erc721Receiver.address, amount, ); @@ -495,79 +558,79 @@ describe('Asset Transfer Proxies', () => { await web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + from: authorized, gas: constants.MAX_TRANSFER_FROM_GAS, }), ); // Verify that no log was emitted by erc721 receiver expect(tx.logs.length).to.be.equal(1); // Verify transfer was successful - const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwnerMakerAsset).to.be.bignumber.equal(erc721Receiver.address); + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.bignumber.equal(erc721Receiver.address); }); - it('should throw if transferring 0 amount of a token', async () => { + it('should revert if transferring 0 amount of a token', async () => { // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress const amount = new BigNumber(0); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await expectTransactionFailedAsync( web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + from: authorized, }), RevertReason.InvalidAmount, ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); }); - it('should throw if transferring > 1 amount of a token', async () => { + it('should revert if transferring > 1 amount of a token', async () => { // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress const amount = new BigNumber(500); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await expectTransactionFailedAsync( web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + from: authorized, }), RevertReason.InvalidAmount, ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); }); - it('should throw if allowances are too low', async () => { + it('should revert if allowances are too low', async () => { // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Remove transfer approval for makerAddress. + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Remove transfer approval for fromAddress. await web3Wrapper.awaitTransactionSuccessAsync( - await erc721Token.approve.sendTransactionAsync(constants.NULL_ADDRESS, erc721MakerTokenId, { - from: makerAddress, + await erc721TokenA.approve.sendTransactionAsync(constants.NULL_ADDRESS, erc721AFromTokenId, { + from: fromAddress, }), constants.AWAIT_TRANSACTION_MINED_MS, ); @@ -575,34 +638,34 @@ describe('Asset Transfer Proxies', () => { const amount = new BigNumber(1); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await expectTransactionFailedAsync( web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + from: authorized, }), RevertReason.TransferFailed, ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); }); - it('should throw if requesting address is not authorized', async () => { + it('should revert if caller is not authorized', async () => { // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress const amount = new BigNumber(1); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await expectTransactionFailedAsync( @@ -613,16 +676,570 @@ describe('Asset Transfer Proxies', () => { }), RevertReason.SenderNotAuthorized, ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); }); }); - - it('should have an id of 0x02571792', async () => { - const proxyId = await erc721Proxy.getProxyId.callAsync(); - const expectedProxyId = '0x02571792'; + }); + describe('MultiAssetProxy', () => { + it('should revert if undefined function is called', async () => { + const undefinedSelector = '0x01020304'; + await expectTransactionFailedWithoutReasonAsync( + web3Wrapper.sendTransactionAsync({ + from: owner, + to: multiAssetProxy.address, + value: constants.ZERO_AMOUNT, + data: undefinedSelector, + }), + ); + }); + it('should have an id of 0x94cfcdd7', async () => { + const proxyId = await multiAssetProxy.getProxyId.callAsync(); + // first 4 bytes of `keccak256('MultiAsset(uint256[],bytes[])')` + const expectedProxyId = '0x94cfcdd7'; expect(proxyId).to.equal(expectedProxyId); }); + describe('transferFrom', () => { + it('should transfer a single ERC20 token', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const amounts = [erc20Amount]; + const nestedAssetData = [erc20AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalAmount = inputAmount.times(erc20Amount); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalAmount), + ); + }); + it('should successfully transfer multiple of the same ERC20 token', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount1 = new BigNumber(10); + const erc20Amount2 = new BigNumber(20); + const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const amounts = [erc20Amount1, erc20Amount2]; + const nestedAssetData = [erc20AssetData1, erc20AssetData2]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalAmount = inputAmount.times(erc20Amount1).plus(inputAmount.times(erc20Amount2)); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalAmount), + ); + }); + it('should successfully transfer multiple different ERC20 tokens', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount1 = new BigNumber(10); + const erc20Amount2 = new BigNumber(20); + const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address); + const amounts = [erc20Amount1, erc20Amount2]; + const nestedAssetData = [erc20AssetData1, erc20AssetData2]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalErc20AAmount = inputAmount.times(erc20Amount1); + const totalErc20BAmount = inputAmount.times(erc20Amount2); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount), + ); + expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount), + ); + expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount), + ); + }); + it('should transfer a single ERC721 token', async () => { + const inputAmount = new BigNumber(1); + const erc721Amount = new BigNumber(1); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc721Amount]; + const nestedAssetData = [erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.equal(toAddress); + }); + it('should successfully transfer multiple of the same ERC721 token', async () => { + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + const erc721AFromTokenId2 = erc721Balances[fromAddress][erc721TokenA.address][1]; + const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const erc721AssetData2 = assetDataUtils.encodeERC721AssetData( + erc721TokenA.address, + erc721AFromTokenId2, + ); + const inputAmount = new BigNumber(1); + const erc721Amount = new BigNumber(1); + const amounts = [erc721Amount, erc721Amount]; + const nestedAssetData = [erc721AssetData1, erc721AssetData2]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset1).to.be.equal(fromAddress); + const ownerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2); + expect(ownerFromAsset2).to.be.equal(fromAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + gas: constants.MAX_TRANSFER_FROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + const newOwnerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2); + expect(newOwnerFromAsset1).to.be.equal(toAddress); + expect(newOwnerFromAsset2).to.be.equal(toAddress); + }); + it('should successfully transfer multiple different ERC721 tokens', async () => { + const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const erc721AssetData2 = assetDataUtils.encodeERC721AssetData(erc721TokenB.address, erc721BFromTokenId); + const inputAmount = new BigNumber(1); + const erc721Amount = new BigNumber(1); + const amounts = [erc721Amount, erc721Amount]; + const nestedAssetData = [erc721AssetData1, erc721AssetData2]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset1).to.be.equal(fromAddress); + const ownerFromAsset2 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId); + expect(ownerFromAsset2).to.be.equal(fromAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + gas: constants.MAX_TRANSFER_FROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + const newOwnerFromAsset2 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId); + expect(newOwnerFromAsset1).to.be.equal(toAddress); + expect(newOwnerFromAsset2).to.be.equal(toAddress); + }); + it('should successfully transfer a combination of ERC20 and ERC721 tokens', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721Amount = new BigNumber(1); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalAmount = inputAmount.times(erc20Amount); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalAmount), + ); + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.equal(toAddress); + }); + it('should successfully transfer tokens and ignore extra assetData', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721Amount = new BigNumber(1); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const extraData = '0102030405060708'; + const assetData = `${assetDataInterface.MultiAsset.getABIEncodedTransactionData( + amounts, + nestedAssetData, + )}${extraData}`; + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalAmount = inputAmount.times(erc20Amount); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalAmount), + ); + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.equal(toAddress); + }); + it('should successfully transfer correct amounts when the `amount` > 1', async () => { + const inputAmount = new BigNumber(100); + const erc20Amount1 = new BigNumber(10); + const erc20Amount2 = new BigNumber(20); + const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address); + const amounts = [erc20Amount1, erc20Amount2]; + const nestedAssetData = [erc20AssetData1, erc20AssetData2]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalErc20AAmount = inputAmount.times(erc20Amount1); + const totalErc20BAmount = inputAmount.times(erc20Amount2); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount), + ); + expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount), + ); + expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount), + ); + }); + it('should successfully transfer a large amount of tokens', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount1 = new BigNumber(10); + const erc20Amount2 = new BigNumber(20); + const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address); + const erc721Amount = new BigNumber(1); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + const erc721AFromTokenId2 = erc721Balances[fromAddress][erc721TokenA.address][1]; + const erc721BFromTokenId2 = erc721Balances[fromAddress][erc721TokenB.address][1]; + const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const erc721AssetData2 = assetDataUtils.encodeERC721AssetData( + erc721TokenA.address, + erc721AFromTokenId2, + ); + const erc721AssetData3 = assetDataUtils.encodeERC721AssetData(erc721TokenB.address, erc721BFromTokenId); + const erc721AssetData4 = assetDataUtils.encodeERC721AssetData( + erc721TokenB.address, + erc721BFromTokenId2, + ); + const amounts = [erc721Amount, erc20Amount1, erc721Amount, erc20Amount2, erc721Amount, erc721Amount]; + const nestedAssetData = [ + erc721AssetData1, + erc20AssetData1, + erc721AssetData2, + erc20AssetData2, + erc721AssetData3, + erc721AssetData4, + ]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset1).to.be.equal(fromAddress); + const ownerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2); + expect(ownerFromAsset2).to.be.equal(fromAddress); + const ownerFromAsset3 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId); + expect(ownerFromAsset3).to.be.equal(fromAddress); + const ownerFromAsset4 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId2); + expect(ownerFromAsset4).to.be.equal(fromAddress); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + gas: constants.MAX_EXECUTE_TRANSACTION_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + const newOwnerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2); + const newOwnerFromAsset3 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId); + const newOwnerFromAsset4 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId2); + expect(newOwnerFromAsset1).to.be.equal(toAddress); + expect(newOwnerFromAsset2).to.be.equal(toAddress); + expect(newOwnerFromAsset3).to.be.equal(toAddress); + expect(newOwnerFromAsset4).to.be.equal(toAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalErc20AAmount = inputAmount.times(erc20Amount1); + const totalErc20BAmount = inputAmount.times(erc20Amount2); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount), + ); + expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount), + ); + expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount), + ); + }); + it('should revert if a single transfer fails', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + // 2 is an invalid erc721 amount + const erc721Amount = new BigNumber(2); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + RevertReason.InvalidAmount, + ); + }); + it('should revert if an AssetProxy is not registered', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721Amount = new BigNumber(1); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const invalidProxyId = '0x12345678'; + const invalidErc721AssetData = `${invalidProxyId}${erc721AssetData.slice(10)}`; + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, invalidErc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + RevertReason.AssetProxyDoesNotExist, + ); + }); + it('should revert if the length of `amounts` does not match the length of `nestedAssetData`', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc20Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + RevertReason.LengthMismatch, + ); + }); + it('should revert if amounts multiplication results in an overflow', async () => { + const inputAmount = new BigNumber(2).pow(128); + const erc20Amount = new BigNumber(2).pow(128); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const amounts = [erc20Amount]; + const nestedAssetData = [erc20AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + RevertReason.Uint256Overflow, + ); + }); + it('should revert if an element of `nestedAssetData` is < 4 bytes long', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721Amount = new BigNumber(1); + const erc721AssetData = '0x123456'; + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + RevertReason.LengthGreaterThan3Required, + ); + }); + it('should revert if caller is not authorized', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721Amount = new BigNumber(1); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: notAuthorized, + }), + RevertReason.SenderNotAuthorized, + ); + }); + }); }); }); // tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index fc8dc5346..9159b0d8f 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/packages/contracts/test/exchange/core.ts @@ -14,6 +14,8 @@ import { DummyNoReturnERC20TokenContract } from '../../generated-wrappers/dummy_ import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy'; import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy'; import { ExchangeCancelEventArgs, ExchangeContract } from '../../generated-wrappers/exchange'; +import { IAssetDataContract } from '../../generated-wrappers/i_asset_data'; +import { MultiAssetProxyContract } from '../../generated-wrappers/multi_asset_proxy'; import { ReentrantERC20TokenContract } from '../../generated-wrappers/reentrant_erc20_token'; import { TestStaticCallReceiverContract } from '../../generated-wrappers/test_static_call_receiver'; import { artifacts } from '../../src/artifacts'; @@ -31,6 +33,11 @@ import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const assetDataInterface = new IAssetDataContract( + artifacts.IAssetData.compilerOutput.abi, + constants.NULL_ADDRESS, + provider, +); // tslint:disable:no-unnecessary-type-assertion describe('Exchange core', () => { let makerAddress: string; @@ -47,6 +54,7 @@ describe('Exchange core', () => { let exchange: ExchangeContract; let erc20Proxy: ERC20ProxyContract; let erc721Proxy: ERC721ProxyContract; + let multiAssetProxy: MultiAssetProxyContract; let maliciousWallet: TestStaticCallReceiverContract; let maliciousValidator: TestStaticCallReceiverContract; @@ -76,31 +84,39 @@ describe('Exchange core', () => { erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + // Deploy AssetProxies, Exchange, tokens, and malicious contracts + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync( + artifacts.MultiAssetProxy, + provider, + txDefaults, + ); const numDummyErc20ToDeploy = 3; [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS, ); - 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); + maliciousWallet = maliciousValidator = await TestStaticCallReceiverContract.deployFrom0xArtifactAsync( + artifacts.TestStaticCallReceiver, + provider, + txDefaults, + ); + reentrantErc20Token = await ReentrantERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.ReentrantERC20Token, + provider, + txDefaults, + exchange.address, + ); + // Configure ERC20Proxy await web3Wrapper.awaitTransactionSuccessAsync( await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: owner, @@ -108,27 +124,64 @@ describe('Exchange core', () => { constants.AWAIT_TRANSACTION_MINED_MS, ); await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + // Configure ERC721Proxy + await web3Wrapper.awaitTransactionSuccessAsync( await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: owner, }), constants.AWAIT_TRANSACTION_MINED_MS, ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); - maliciousWallet = maliciousValidator = await TestStaticCallReceiverContract.deployFrom0xArtifactAsync( - artifacts.TestStaticCallReceiver, - provider, - txDefaults, + // Configure MultiAssetProxy + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, ); - reentrantErc20Token = await ReentrantERC20TokenContract.deployFrom0xArtifactAsync( - artifacts.ReentrantERC20Token, - provider, - txDefaults, - exchange.address, + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc721Proxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, ); + // Configure Exchange + exchangeWrapper = new ExchangeWrapper(exchange, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(multiAssetProxy.address, owner); + + // Configure ERC20 tokens + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + // Configure ERC721 tokens + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address]; + erc721TakerAssetIds = erc721Balances[takerAddress][erc721Token.address]; + + // Configure order defaults defaultMakerAssetAddress = erc20TokenA.address; defaultTakerAssetAddress = erc20TokenB.address; - const defaultOrderParams = { ...constants.STATIC_ORDER_PARAMS, exchangeAddress: exchange.address, @@ -707,6 +760,292 @@ describe('Exchange core', () => { }); }); + describe('Testing exchange of multiple assets', () => { + it('should allow multiple assets to be exchanged for a single asset', async () => { + const makerAmounts = [new BigNumber(10), new BigNumber(20)]; + const makerNestedAssetData = [ + assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + assetDataUtils.encodeERC20AssetData(erc20TokenB.address), + ]; + const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData( + makerAmounts, + makerNestedAssetData, + ); + const makerAssetAmount = new BigNumber(1); + const takerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const takerAssetAmount = new BigNumber(10); + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + takerAssetData, + makerAssetAmount, + takerAssetAmount, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }); + + const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + + const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + expect(finalMakerBalanceA).to.be.bignumber.equal( + initialMakerBalanceA.minus(makerAmounts[0].times(makerAssetAmount)), + ); + expect(finalMakerBalanceB).to.be.bignumber.equal( + initialMakerBalanceB.minus(makerAmounts[1].times(makerAssetAmount)), + ); + expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.plus(takerAssetAmount)); + expect(finalTakerBalanceA).to.be.bignumber.equal( + initialTakerBalanceA.plus(makerAmounts[0].times(makerAssetAmount)), + ); + expect(finalTakerBalanceB).to.be.bignumber.equal( + initialTakerBalanceB.plus(makerAmounts[1].times(makerAssetAmount)), + ); + expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(takerAssetAmount)); + }); + it('should allow multiple assets to be exchanged for multiple assets', async () => { + const makerAmounts = [new BigNumber(10), new BigNumber(20)]; + const makerNestedAssetData = [ + assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + assetDataUtils.encodeERC20AssetData(erc20TokenB.address), + ]; + const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData( + makerAmounts, + makerNestedAssetData, + ); + const makerAssetAmount = new BigNumber(1); + const takerAmounts = [new BigNumber(10), new BigNumber(1)]; + const takerAssetId = erc721TakerAssetIds[0]; + const takerNestedAssetData = [ + assetDataUtils.encodeERC20AssetData(zrxToken.address), + assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId), + ]; + const takerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData( + takerAmounts, + takerNestedAssetData, + ); + const takerAssetAmount = new BigNumber(1); + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + takerAssetData, + makerAssetAmount, + takerAssetAmount, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }); + + const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId); + expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + + const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + const finalOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId); + + expect(finalMakerBalanceA).to.be.bignumber.equal( + initialMakerBalanceA.minus(makerAmounts[0].times(makerAssetAmount)), + ); + expect(finalMakerBalanceB).to.be.bignumber.equal( + initialMakerBalanceB.minus(makerAmounts[1].times(makerAssetAmount)), + ); + expect(finalMakerZrxBalance).to.be.bignumber.equal( + initialMakerZrxBalance.plus(takerAmounts[0].times(takerAssetAmount)), + ); + expect(finalTakerBalanceA).to.be.bignumber.equal( + initialTakerBalanceA.plus(makerAmounts[0].times(makerAssetAmount)), + ); + expect(finalTakerBalanceB).to.be.bignumber.equal( + initialTakerBalanceB.plus(makerAmounts[1].times(makerAssetAmount)), + ); + expect(finalTakerZrxBalance).to.be.bignumber.equal( + initialTakerZrxBalance.minus(takerAmounts[0].times(takerAssetAmount)), + ); + expect(finalOwnerTakerAsset).to.be.equal(makerAddress); + }); + it('should allow an order selling multiple assets to be partially filled', async () => { + const makerAmounts = [new BigNumber(10), new BigNumber(20)]; + const makerNestedAssetData = [ + assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + assetDataUtils.encodeERC20AssetData(erc20TokenB.address), + ]; + const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData( + makerAmounts, + makerNestedAssetData, + ); + const makerAssetAmount = new BigNumber(30); + const takerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const takerAssetAmount = new BigNumber(10); + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + takerAssetData, + makerAssetAmount, + takerAssetAmount, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }); + + const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + const takerAssetFillAmount = takerAssetAmount.dividedToIntegerBy(2); + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { + takerAssetFillAmount, + }); + + const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + expect(finalMakerBalanceA).to.be.bignumber.equal( + initialMakerBalanceA.minus( + makerAmounts[0].times( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalMakerBalanceB).to.be.bignumber.equal( + initialMakerBalanceB.minus( + makerAmounts[1].times( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalMakerZrxBalance).to.be.bignumber.equal( + initialMakerZrxBalance.plus( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ); + expect(finalTakerBalanceA).to.be.bignumber.equal( + initialTakerBalanceA.plus( + makerAmounts[0].times( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalTakerBalanceB).to.be.bignumber.equal( + initialTakerBalanceB.plus( + makerAmounts[1].times( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalTakerZrxBalance).to.be.bignumber.equal( + initialTakerZrxBalance.minus( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ); + }); + it('should allow an order buying multiple assets to be partially filled', async () => { + const takerAmounts = [new BigNumber(10), new BigNumber(20)]; + const takerNestedAssetData = [ + assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + assetDataUtils.encodeERC20AssetData(erc20TokenB.address), + ]; + const takerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData( + takerAmounts, + takerNestedAssetData, + ); + const takerAssetAmount = new BigNumber(30); + const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const makerAssetAmount = new BigNumber(10); + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + takerAssetData, + makerAssetAmount, + takerAssetAmount, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }); + + const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + const takerAssetFillAmount = takerAssetAmount.dividedToIntegerBy(2); + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { + takerAssetFillAmount, + }); + + const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + expect(finalMakerBalanceA).to.be.bignumber.equal( + initialMakerBalanceA.plus( + takerAmounts[0].times( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalMakerBalanceB).to.be.bignumber.equal( + initialMakerBalanceB.plus( + takerAmounts[1].times( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalMakerZrxBalance).to.be.bignumber.equal( + initialMakerZrxBalance.minus( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ); + expect(finalTakerBalanceA).to.be.bignumber.equal( + initialTakerBalanceA.minus( + takerAmounts[0].times( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalTakerBalanceB).to.be.bignumber.equal( + initialTakerBalanceB.minus( + takerAmounts[1].times( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalTakerZrxBalance).to.be.bignumber.equal( + initialTakerZrxBalance.plus( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ); + }); + }); + describe('getOrderInfo', () => { beforeEach(async () => { signedOrder = await orderFactory.newSignedOrderAsync(); diff --git a/packages/contracts/test/utils/constants.ts b/packages/contracts/test/utils/constants.ts index cd21330e9..d2c3ab512 100644 --- a/packages/contracts/test/utils/constants.ts +++ b/packages/contracts/test/utils/constants.ts @@ -35,7 +35,7 @@ export const constants = { DUMMY_TOKEN_TOTAL_SUPPLY: new BigNumber(0), NULL_BYTES: '0x', NUM_DUMMY_ERC20_TO_DEPLOY: 3, - NUM_DUMMY_ERC721_TO_DEPLOY: 1, + NUM_DUMMY_ERC721_TO_DEPLOY: 2, NUM_ERC721_TOKENS_TO_MINT: 2, NULL_ADDRESS: '0x0000000000000000000000000000000000000000', UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), diff --git a/packages/contracts/test/utils/erc721_wrapper.ts b/packages/contracts/test/utils/erc721_wrapper.ts index 3ef4e701d..e9da553d0 100644 --- a/packages/contracts/test/utils/erc721_wrapper.ts +++ b/packages/contracts/test/utils/erc721_wrapper.ts @@ -29,7 +29,8 @@ export class ERC721Wrapper { this._contractOwnerAddress = contractOwnerAddress; } public async deployDummyTokensAsync(): Promise<DummyERC721TokenContract[]> { - for (let i = 0; i < constants.NUM_DUMMY_ERC721_TO_DEPLOY; i++) { + // tslint:disable-next-line:no-unused-variable + for (const i of _.times(constants.NUM_DUMMY_ERC721_TO_DEPLOY)) { this._dummyTokenContracts.push( await DummyERC721TokenContract.deployFrom0xArtifactAsync( artifacts.DummyERC721Token, @@ -61,7 +62,8 @@ export class ERC721Wrapper { 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++) { + // tslint:disable-next-line:no-unused-variable + for (const i of _.times(constants.NUM_ERC721_TOKENS_TO_MINT)) { const tokenId = generatePseudoRandomSalt(); await this.mintAsync(dummyTokenContract.address, tokenId, tokenOwnerAddress); if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress])) { diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json index 8b29365cc..e0f85079a 100644 --- a/packages/contracts/tsconfig.json +++ b/packages/contracts/tsconfig.json @@ -26,6 +26,7 @@ "./generated-artifacts/IWallet.json", "./generated-artifacts/InvalidERC721Receiver.json", "./generated-artifacts/MixinAuthorizable.json", + "./generated-artifacts/MultiAssetProxy.json", "./generated-artifacts/MultiSigWallet.json", "./generated-artifacts/MultiSigWalletWithTimeLock.json", "./generated-artifacts/OrderValidator.json", diff --git a/packages/instant/src/components/install_wallet_panel_content.tsx b/packages/instant/src/components/install_wallet_panel_content.tsx index 88c26f59c..481d82da0 100644 --- a/packages/instant/src/components/install_wallet_panel_content.tsx +++ b/packages/instant/src/components/install_wallet_panel_content.tsx @@ -8,7 +8,9 @@ import { } from '../constants'; import { ColorOption } from '../style/theme'; import { Browser } from '../types'; +import { analytics } from '../util/analytics'; import { envUtil } from '../util/env'; +import { util } from '../util/util'; import { MetaMaskLogo } from './meta_mask_logo'; import { StandardPanelContent, StandardPanelContentProps } from './standard_panel_content'; @@ -45,6 +47,10 @@ export class InstallWalletPanelContent extends React.Component<InstallWalletPane default: break; } + const onActionClick = () => { + analytics.trackInstallWalletModalClickedGet(); + util.createOpenUrlInNewWindow(actionUrl)(); + }; return { image: <MetaMaskLogo width={85} height={80} />, title: 'Install MetaMask', @@ -52,10 +58,11 @@ export class InstallWalletPanelContent extends React.Component<InstallWalletPane moreInfoSettings: { href: META_MASK_SITE_URL, text: 'What is MetaMask?', + onClick: analytics.trackInstallWalletModalClickedExplanation, }, action: ( <Button - href={actionUrl} + onClick={onActionClick} width="100%" fontColor={ColorOption.white} backgroundColor={ColorOption.darkOrange} diff --git a/packages/instant/src/components/standard_panel_content.tsx b/packages/instant/src/components/standard_panel_content.tsx index 582b3318e..79b7bff24 100644 --- a/packages/instant/src/components/standard_panel_content.tsx +++ b/packages/instant/src/components/standard_panel_content.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { ColorOption } from '../style/theme'; +import { util } from '../util/util'; import { Container } from './ui/container'; import { Flex } from './ui/flex'; @@ -9,6 +10,7 @@ import { Text } from './ui/text'; export interface MoreInfoSettings { text: string; href: string; + onClick?: () => void; } export interface StandardPanelContentProps { @@ -21,6 +23,15 @@ export interface StandardPanelContentProps { const SPACING_BETWEEN_PX = '20px'; +const onMoreInfoClick = (href: string, onClick?: () => void) => { + return () => { + if (onClick) { + onClick(); + } + util.createOpenUrlInNewWindow(href)(); + }; +}; + export const StandardPanelContent: React.StatelessComponent<StandardPanelContentProps> = ({ image, title, @@ -50,7 +61,7 @@ export const StandardPanelContent: React.StatelessComponent<StandardPanelContent fontSize="13px" textDecorationLine="underline" fontColor={ColorOption.lightGrey} - href={moreInfoSettings.href} + onClick={onMoreInfoClick(moreInfoSettings.href, moreInfoSettings.onClick)} > {moreInfoSettings.text} </Text> diff --git a/packages/instant/src/containers/connected_account_payment_method.ts b/packages/instant/src/containers/connected_account_payment_method.ts index cdeb49a25..e9327a288 100644 --- a/packages/instant/src/containers/connected_account_payment_method.ts +++ b/packages/instant/src/containers/connected_account_payment_method.ts @@ -11,7 +11,7 @@ import { import { Action, actions } from '../redux/actions'; import { asyncData } from '../redux/async_data'; import { State } from '../redux/reducer'; -import { Network, Omit, OperatingSystem, ProviderState, StandardSlidingPanelContent } from '../types'; +import { Network, Omit, OperatingSystem, ProviderState, StandardSlidingPanelContent, WalletSuggestion } from '../types'; import { analytics } from '../util/analytics'; import { envUtil } from '../util/env'; @@ -60,23 +60,28 @@ const mergeProps = ( onUnlockWalletClick: () => connectedDispatch.unlockWalletAndDispatchToStore(connectedState.providerState), onInstallWalletClick: () => { const isMobile = envUtil.isMobileOperatingSystem(); - if (!isMobile) { + const walletSuggestion: WalletSuggestion = isMobile + ? WalletSuggestion.CoinbaseWallet + : WalletSuggestion.MetaMask; + + analytics.trackInstallWalletClicked(walletSuggestion); + if (walletSuggestion === WalletSuggestion.MetaMask) { connectedDispatch.openInstallWalletPanel(); - return; - } - const operatingSystem = envUtil.getOperatingSystem(); - let url = COINBASE_WALLET_SITE_URL; - switch (operatingSystem) { - case OperatingSystem.Android: - url = COINBASE_WALLET_ANDROID_APP_STORE_URL; - break; - case OperatingSystem.iOS: - url = COINBASE_WALLET_IOS_APP_STORE_URL; - break; - default: - break; + } else { + const operatingSystem = envUtil.getOperatingSystem(); + let url = COINBASE_WALLET_SITE_URL; + switch (operatingSystem) { + case OperatingSystem.Android: + url = COINBASE_WALLET_ANDROID_APP_STORE_URL; + break; + case OperatingSystem.iOS: + url = COINBASE_WALLET_IOS_APP_STORE_URL; + break; + default: + break; + } + window.open(url, '_blank'); } - window.open(url, '_blank'); }, }); diff --git a/packages/instant/src/redux/analytics_middleware.ts b/packages/instant/src/redux/analytics_middleware.ts index 8aa76eb77..3dc5fe924 100644 --- a/packages/instant/src/redux/analytics_middleware.ts +++ b/packages/instant/src/redux/analytics_middleware.ts @@ -3,7 +3,7 @@ import * as _ from 'lodash'; import { Middleware } from 'redux'; import { ETH_DECIMALS } from '../constants'; -import { Account, AccountState } from '../types'; +import { Account, AccountState, StandardSlidingPanelContent } from '../types'; import { analytics } from '../util/analytics'; import { Action, ActionTypes } from './actions'; @@ -77,6 +77,18 @@ export const analyticsMiddleware: Middleware = store => next => middlewareAction }); } break; + case ActionTypes.OPEN_STANDARD_SLIDING_PANEL: + const openSlidingContent = curState.standardSlidingPanelSettings.content; + if (openSlidingContent === StandardSlidingPanelContent.InstallWallet) { + analytics.trackInstallWalletModalOpened(); + } + break; + case ActionTypes.CLOSE_STANDARD_SLIDING_PANEL: + const closeSlidingContent = curState.standardSlidingPanelSettings.content; + if (closeSlidingContent === StandardSlidingPanelContent.InstallWallet) { + analytics.trackInstallWalletModalClosed(); + } + break; } return nextAction; diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index 999d50fed..4ad9c9d4f 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -149,6 +149,11 @@ export enum Browser { Other = 'OTHER', } +export enum WalletSuggestion { + CoinbaseWallet = 'Coinbase Wallet', + MetaMask = 'MetaMask', +} + export enum OperatingSystem { Android = 'ANDROID', iOS = 'IOS', diff --git a/packages/instant/src/util/analytics.ts b/packages/instant/src/util/analytics.ts index 5bc9bb385..d44998666 100644 --- a/packages/instant/src/util/analytics.ts +++ b/packages/instant/src/util/analytics.ts @@ -1,7 +1,7 @@ import { BuyQuote } from '@0x/asset-buyer'; import * as _ from 'lodash'; -import { AffiliateInfo, Asset, Network, OrderSource, ProviderState } from '../types'; +import { AffiliateInfo, Asset, Network, OrderSource, ProviderState, WalletSuggestion } from '../types'; import { EventProperties, heapUtil } from './heap'; @@ -33,6 +33,11 @@ enum EventNames { BUY_TX_SUBMITTED = 'Buy - Tx Submitted', BUY_TX_SUCCEEDED = 'Buy - Tx Succeeded', BUY_TX_FAILED = 'Buy - Tx Failed', + INSTALL_WALLET_CLICKED = 'Install Wallet - Clicked', + INSTALL_WALLET_MODAL_OPENED = 'Install Wallet - Modal - Opened', + INSTALL_WALLET_MODAL_CLICKED_EXPLANATION = 'Install Wallet - Modal - Clicked Explanation', + INSTALL_WALLET_MODAL_CLICKED_GET = 'Install Wallet - Modal - Clicked Get', + INSTALL_WALLET_MODAL_CLOSED = 'Install Wallet - Modal - Closed', TOKEN_SELECTOR_OPENED = 'Token Selector - Opened', TOKEN_SELECTOR_CLOSED = 'Token Selector - Closed', TOKEN_SELECTOR_CHOSE = 'Token Selector - Chose', @@ -170,6 +175,14 @@ export const analytics = { expectedTxTimeMs: expectedEndTimeUnix - startTimeUnix, actualTxTimeMs: new Date().getTime() - startTimeUnix, }), + trackInstallWalletClicked: (walletSuggestion: WalletSuggestion) => + trackingEventFnWithPayload(EventNames.INSTALL_WALLET_CLICKED)({ walletSuggestion }), + trackInstallWalletModalClickedExplanation: trackingEventFnWithoutPayload( + EventNames.INSTALL_WALLET_MODAL_CLICKED_EXPLANATION, + ), + trackInstallWalletModalClickedGet: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_CLICKED_GET), + trackInstallWalletModalOpened: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_OPENED), + trackInstallWalletModalClosed: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_CLOSED), trackTokenSelectorOpened: trackingEventFnWithoutPayload(EventNames.TOKEN_SELECTOR_OPENED), trackTokenSelectorClosed: (closedVia: TokenSelectorClosedVia) => trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_CLOSED)({ closedVia }), diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json index 0b32b60f0..53b24aff0 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "1.4.0", + "changes": [ + { + "note": "Add `LengthMismatch` and `LengthGreaterThan3Required` revert reasons", + "pr": 1224 + } + ] + }, + { "version": "1.3.0", "changes": [ { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 1a86f45e6..26d8f8e22 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -195,6 +195,7 @@ export enum RevertReason { FailedExecution = 'FAILED_EXECUTION', AssetProxyAlreadyExists = 'ASSET_PROXY_ALREADY_EXISTS', LengthGreaterThan0Required = 'LENGTH_GREATER_THAN_0_REQUIRED', + LengthGreaterThan3Required = 'LENGTH_GREATER_THAN_3_REQUIRED', LengthGreaterThan131Required = 'LENGTH_GREATER_THAN_131_REQUIRED', Length0Required = 'LENGTH_0_REQUIRED', Length65Required = 'LENGTH_65_REQUIRED', @@ -209,6 +210,7 @@ export enum RevertReason { MakerNotWhitelisted = 'MAKER_NOT_WHITELISTED', TakerNotWhitelisted = 'TAKER_NOT_WHITELISTED', AssetProxyDoesNotExist = 'ASSET_PROXY_DOES_NOT_EXIST', + LengthMismatch = 'LENGTH_MISMATCH', LibBytesGreaterThanZeroLengthRequired = 'GREATER_THAN_ZERO_LENGTH_REQUIRED', LibBytesGreaterOrEqualTo4LengthRequired = 'GREATER_OR_EQUAL_TO_4_LENGTH_REQUIRED', LibBytesGreaterOrEqualTo20LengthRequired = 'GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED', |