From 78d81f193f3b9358ab86819f83c76b8bcd52a9c9 Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Tue, 10 Apr 2018 17:53:34 -0700 Subject: Asset Proxy Dispatcher --- packages/contracts/package.json | 8 +- .../AssetProxyDispatcher/AssetProxyDispatcher.sol | 86 ++++ .../protocol/AssetProxyDispatcher/IAssetProxy.sol | 34 ++ .../AssetProxyDispatcher/IAssetProxyDispatcher.sol | 51 +++ .../proxies/ERC20TransferProxy.sol | 80 ++++ .../proxies/ERC20TransferProxy_v1.sol | 89 ++++ .../proxies/ERC721TransferProxy.sol | 94 ++++ .../current/protocol/Exchange/Exchange.sol | 7 +- .../current/protocol/Exchange/IExchange.sol | 5 - .../current/protocol/Exchange/LibOrder.sol | 10 +- .../current/protocol/Exchange/LibPartialAmount.sol | 2 - .../protocol/Exchange/MixinSettlementProxy.sol | 77 ++-- .../protocol/Exchange/MixinWrapperFunctions.sol | 228 +++++++--- .../test/DummyERC721Token/DummyERC721Token.sol | 46 ++ .../current/utils/Authorizable/Authorizable.sol | 109 +++++ .../current/utils/Authorizable/IAuthorizable.sol | 53 +++ .../contracts/current/utils/LibBytes/LibBytes.sol | 126 ++++++ .../contracts/current/utils/Ownable/IOwnable.sol | 13 + .../contracts/current/utils/Ownable/Ownable.sol | 4 +- packages/contracts/src/utils/asset_proxy_utils.ts | 67 +++ packages/contracts/src/utils/constants.ts | 1 + packages/contracts/src/utils/crypto.ts | 2 + packages/contracts/src/utils/order_utils.ts | 6 + packages/contracts/src/utils/types.ts | 20 +- .../test/asset_proxy_dispatcher/dispatcher.ts | 339 ++++++++++++++ .../test/asset_proxy_dispatcher/proxies.ts | 407 +++++++++++++++++ packages/contracts/test/exchange/core.ts | 503 ++++++++++++++++++++- packages/contracts/test/exchange/helpers.ts | 47 +- packages/contracts/test/exchange/wrapper.ts | 90 +++- yarn.lock | 33 +- 30 files changed, 2473 insertions(+), 164 deletions(-) create mode 100644 packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/AssetProxyDispatcher.sol create mode 100644 packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/IAssetProxy.sol create mode 100644 packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/IAssetProxyDispatcher.sol create mode 100644 packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC20TransferProxy.sol create mode 100644 packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC20TransferProxy_v1.sol create mode 100644 packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC721TransferProxy.sol create mode 100644 packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol create mode 100644 packages/contracts/src/contracts/current/utils/Authorizable/Authorizable.sol create mode 100644 packages/contracts/src/contracts/current/utils/Authorizable/IAuthorizable.sol create mode 100644 packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol create mode 100644 packages/contracts/src/contracts/current/utils/Ownable/IOwnable.sol create mode 100644 packages/contracts/src/utils/asset_proxy_utils.ts create mode 100644 packages/contracts/test/asset_proxy_dispatcher/dispatcher.ts create mode 100644 packages/contracts/test/asset_proxy_dispatcher/proxies.ts diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 78d50d888..be65787d5 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -26,8 +26,9 @@ "test:circleci": "yarn test:coverage" }, "config": { - "abis": "../migrations/src/artifacts/@(DummyToken|TokenTransferProxy|Exchange|TokenRegistry|MultiSigWallet|MultiSigWalletWithTimeLock|MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress|TokenRegistry|ZRXToken).json", - "contracts": "Exchange,DummyToken,ZRXToken,Token,WETH9,TokenTransferProxy,MultiSigWallet,MultiSigWalletWithTimeLock,MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress,MaliciousToken,TokenRegistry" + "abis": "../migrations/src/artifacts/@(DummyToken|TokenTransferProxy|Exchange|TokenRegistry|MultiSigWallet|MultiSigWalletWithTimeLock|MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress|TokenRegistry|ZRXToken|AssetProxyDispatcher|ERC20TransferProxy_v1|ERC20TransferProxy|ERC721TransferProxy|DummyERC721Token).json", + "contracts": "Exchange,DummyToken,ZRXToken,Token,WETH9,TokenTransferProxy,MultiSigWallet,MultiSigWalletWithTimeLock,MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress,MaliciousToken,TokenRegistry,AssetProxyDispatcher,ERC20TransferProxy_v1,ERC20TransferProxy,ERC721TransferProxy,DummyERC721Token", + "dirs": "src/contracts,zeppelin:../../node_modules/zeppelin-solidity" }, "repository": { "type": "git", @@ -71,6 +72,7 @@ "ethereumjs-util": "^5.1.1", "ethers-contracts": "^2.2.1", "lodash": "^4.17.4", - "web3": "^0.20.0" + "web3": "^0.20.0", + "zeppelin-solidity": "^1.8.0" } } diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/AssetProxyDispatcher.sol b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/AssetProxyDispatcher.sol new file mode 100644 index 000000000..cce330818 --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/AssetProxyDispatcher.sol @@ -0,0 +1,86 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.21; + +import "./IAssetProxyDispatcher.sol"; +import "./IAssetProxy.sol"; +import "../../utils/Ownable/Ownable.sol"; +import "../../utils/Authorizable/Authorizable.sol"; + +contract AssetProxyDispatcher is + Ownable, + Authorizable, + IAssetProxyDispatcher +{ + // Mapping from Asset Proxy Id's to their respective Asset Proxy + mapping (uint8 => IAssetProxy) public assetProxies; + + /// @dev Delegates transfer to the corresponding asset proxy. + /// @param assetMetadata Byte array encoded for the respective asset proxy. + /// @param from Address to transfer token from. + /// @param to Address to transfer token to. + /// @param amount Amount of token to transfer. + function transferFrom( + bytes assetMetadata, + address from, + address to, + uint256 amount) + public + onlyAuthorized + { + // Lookup asset proxy + require(assetMetadata.length >= 1); + uint8 assetProxyId = uint8(assetMetadata[0]); + IAssetProxy assetProxy = assetProxies[assetProxyId]; + + // Dispatch transfer to asset proxy + // transferFrom will either succeed or throw. + assetProxy.transferFrom(assetMetadata, from, to, amount); + } + + /// @dev Registers a new asset proxy. + /// @param assetProxyId Id of the asset proxy. + /// @param newAssetProxyAddress Address of the asset proxy contract to register. + /// @param currentAssetProxyAddress Address of existing asset proxy to overwrite. + function setAssetProxy( + uint8 assetProxyId, + address newAssetProxyAddress, + address currentAssetProxyAddress) + public + onlyOwner + { + // Ensure any existing asset proxy is not unintentionally overwritten + require(currentAssetProxyAddress == address(assetProxies[assetProxyId])); + + // Store asset proxy and log registration + assetProxies[assetProxyId] = IAssetProxy(newAssetProxyAddress); + emit AssetProxyChanged(assetProxyId, newAssetProxyAddress, currentAssetProxyAddress); + } + + /// @dev Gets an asset proxy. + /// @param assetProxyId Id of the asset proxy. + /// @return The asset proxy registered to assetProxyId. Returns 0x0 if no proxy is registered. + function getAssetProxy(uint8 assetProxyId) + public view + returns (IAssetProxy) + { + IAssetProxy assetProxy = assetProxies[assetProxyId]; + return assetProxy; + } +} diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/IAssetProxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/IAssetProxy.sol new file mode 100644 index 000000000..5c5f7e605 --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/IAssetProxy.sol @@ -0,0 +1,34 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.21; + +contract IAssetProxy { + + /// @dev Transfers assets. Either succeeds or throws. + /// @param assetMetadata Byte array encoded for the respective asset proxy. + /// @param from Address to transfer token from. + /// @param to Address to transfer token to. + /// @param amount Amount of token to transfer. + function transferFrom( + bytes assetMetadata, + address from, + address to, + uint256 amount) + public; +} diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/IAssetProxyDispatcher.sol b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/IAssetProxyDispatcher.sol new file mode 100644 index 000000000..9fe7b49a3 --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/IAssetProxyDispatcher.sol @@ -0,0 +1,51 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.21; + +import "./IAssetProxy.sol"; +import "../../utils/Authorizable/IAuthorizable.sol"; + +contract IAssetProxyDispatcher is + IAuthorizable, + IAssetProxy +{ + // Logs registration of new asset proxy + event AssetProxyChanged( + uint8 id, + address newAssetClassAddress, + address oldAssetClassAddress + ); + + /// @dev Sets a new asset proxy. + /// @param assetProxyId Id of the asset proxy. + /// @param newAssetProxyAddress Address of the asset proxy contract to register. + /// @param currentAssetProxyAddress Address of existing asset proxy to overwrite. + function setAssetProxy( + uint8 assetProxyId, + address newAssetProxyAddress, + address currentAssetProxyAddress) + public; + + /// @dev Gets an asset proxy. + /// @param assetProxyId Id of the asset proxy. + /// @return The asset proxy registered to assetProxyId. Returns 0x0 if no proxy is registered. + function getAssetProxy(uint8 assetProxyId) + public view + returns (IAssetProxy); +} diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC20TransferProxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC20TransferProxy.sol new file mode 100644 index 000000000..61fcd9d00 --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC20TransferProxy.sol @@ -0,0 +1,80 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.21; + +import "../IAssetProxy.sol"; +import "../../../utils/LibBytes/LibBytes.sol"; +import "../../../utils/Authorizable/Authorizable.sol"; +import { Token_v1 as ERC20Token } from "../../../../previous/Token/Token_v1.sol"; + +contract ERC20TransferProxy is + LibBytes, + Authorizable, + IAssetProxy +{ + + /// @dev Transfers ERC20 tokens. + /// @param assetMetadata Byte array encoded for the respective asset proxy. + /// @param from Address to transfer token from. + /// @param to Address to transfer token to. + /// @param amount Amount of token to transfer. + function transferFrom( + bytes assetMetadata, + address from, + address to, + uint256 amount) + public + onlyAuthorized + { + address token = decodeMetadata(assetMetadata); + bool success = ERC20Token(token).transferFrom(from, to, amount); + require(success == true); + } + + /// @dev Encodes ERC20 byte array for the ERC20 asset proxy. + /// @param assetProxyId Id of the asset proxy. + /// @param tokenAddress Address of the asset. + /// @return assetMetadata Byte array encoded for the ERC20 asset proxy. + function encodeMetadata( + uint8 assetProxyId, + address tokenAddress) + public pure + returns (bytes assetMetadata) + { + // 0 is reserved as invalid proxy id + require(assetProxyId != 0); + + // Encode fields into a byte array + assetMetadata = new bytes(21); + assetMetadata[0] = byte(assetProxyId); + writeAddress(tokenAddress, assetMetadata, 1); + return assetMetadata; + } + + /// @dev Decodes ERC20-encoded byte array for the ERC20 asset proxy. + /// @param assetMetadata Byte array encoded for the ERC20 asset proxy. + /// @return tokenAddress Address of ERC20 token. + function decodeMetadata(bytes assetMetadata) + public pure + returns (address tokenAddress) + { + require(assetMetadata.length == 21); + return readAddress(assetMetadata, 1); + } +} diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC20TransferProxy_v1.sol b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC20TransferProxy_v1.sol new file mode 100644 index 000000000..6b19f1a52 --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC20TransferProxy_v1.sol @@ -0,0 +1,89 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.21; + +import "../IAssetProxy.sol"; +import "../../../utils/LibBytes/LibBytes.sol"; +import "../../TokenTransferProxy/ITokenTransferProxy.sol"; +import "../../../utils/Authorizable/Authorizable.sol"; + +contract ERC20TransferProxy_v1 is + LibBytes, + Authorizable, + IAssetProxy +{ + ITokenTransferProxy TRANSFER_PROXY; + + /// @dev Contract constructor. + /// @param tokenTransferProxyContract erc20 token transfer proxy contract. + function ERC20TransferProxy_v1(ITokenTransferProxy tokenTransferProxyContract) + public + { + TRANSFER_PROXY = tokenTransferProxyContract; + } + + /// @dev Transfers ERC20 tokens. + /// @param assetMetadata Byte array encoded for the respective asset proxy. + /// @param from Address to transfer token from. + /// @param to Address to transfer token to. + /// @param amount Amount of token to transfer. + function transferFrom( + bytes assetMetadata, + address from, + address to, + uint256 amount) + public + onlyAuthorized + { + address token = decodeMetadata(assetMetadata); + bool success = TRANSFER_PROXY.transferFrom(token, from, to, amount); + require(success == true); + } + + /// @dev Encodes ERC20 byte array for the ERC20 asset proxy. + /// @param assetProxyId Id of the asset proxy. + /// @param tokenAddress Address of the asset. + /// @return assetMetadata Byte array encoded for the ERC20 asset proxy. + function encodeMetadata( + uint8 assetProxyId, + address tokenAddress) + public pure + returns (bytes assetMetadata) + { + // 0 is reserved as invalid proxy id + require(assetProxyId != 0); + + // Encode fields into a byte array + assetMetadata = new bytes(21); + assetMetadata[0] = byte(assetProxyId); + writeAddress(tokenAddress, assetMetadata, 1); + return assetMetadata; + } + + /// @dev Decodes ERC20-encoded byte array for the ERC20 asset proxy. + /// @param assetMetadata Byte array encoded for the ERC20 asset proxy. + /// @return tokenAddress Address of ERC20 token. + function decodeMetadata(bytes assetMetadata) + public pure + returns (address tokenAddress) + { + require(assetMetadata.length == 21); + return readAddress(assetMetadata, 1); + } +} diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC721TransferProxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC721TransferProxy.sol new file mode 100644 index 000000000..51f027a56 --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/AssetProxyDispatcher/proxies/ERC721TransferProxy.sol @@ -0,0 +1,94 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.21; + +import "../IAssetProxy.sol"; +import "../../../utils/LibBytes/LibBytes.sol"; +import "../../../utils/Authorizable/Authorizable.sol"; +import "/zeppelin/contracts/token/ERC721/ERC721Token.sol"; + + +contract ERC721TransferProxy is + LibBytes, + Authorizable, + IAssetProxy +{ + + /// @dev Transfers ERC20 tokens. + /// @param assetMetadata Byte array encoded for the respective asset proxy. + /// @param from Address to transfer token from. + /// @param to Address to transfer token to. + /// @param amount Amount of token to transfer. + function transferFrom( + bytes assetMetadata, + address from, + address to, + uint256 amount) + public + onlyAuthorized + { + // Decode metadata + address token; + uint256 tokenId; + (token, tokenId) = decodeMetadata(assetMetadata); + + // There exists only 1 of each token. + require(amount == 1); + + // Call ERC721 contract. Either succeeds or throws. + ERC721Token(token).transferFrom(from, to, tokenId); + } + + /// @dev Encodes ERC721 byte array for the ERC20 asset proxy. + /// @param assetProxyId Id of the asset proxy. + /// @param tokenAddress Address of the asset. + /// @param tokenId Id of ERC721 token. + /// @return assetMetadata Byte array encoded for the ERC721 asset proxy. + function encodeMetadata( + uint8 assetProxyId, + address tokenAddress, + uint256 tokenId) + public pure + returns (bytes assetMetadata) + { + // 0 is reserved as invalid proxy id + require(assetProxyId != 0); + + // Encode fields into a byte array + assetMetadata = new bytes(53); + assetMetadata[0] = byte(assetProxyId); + writeAddress(tokenAddress, assetMetadata, 1); + writeUint256(tokenId, assetMetadata, 21); + return assetMetadata; + } + + /// @dev Decodes ERC721-encoded byte array for the ERC721 asset proxy. + /// @param assetMetadata Byte array encoded for the ERC721 asset proxy. + /// @return tokenAddress Address of ERC721 token. + /// @return tokenId Id of ERC721 token. + function decodeMetadata(bytes assetMetadata) + public pure + returns (address tokenAddress, uint256 tokenId) + { + require(assetMetadata.length == 53); + tokenAddress = readAddress(assetMetadata, 1); + tokenId = readUint256(assetMetadata, 21); + return (tokenAddress, tokenId); + } +} diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol b/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol index 7a705a0ee..13623894a 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol @@ -23,6 +23,8 @@ import "./MixinExchangeCore.sol"; import "./MixinSignatureValidator.sol"; import "./MixinSettlementProxy.sol"; import "./MixinWrapperFunctions.sol"; +import "../AssetProxyDispatcher/IAssetProxyDispatcher.sol"; +import "../TokenTransferProxy/ITokenTransferProxy.sol"; contract Exchange is MixinExchangeCore, @@ -34,11 +36,12 @@ contract Exchange is function Exchange( IToken _zrxToken, - ITokenTransferProxy _tokenTransferProxy) + bytes _zrxProxyMetadata, + IAssetProxyDispatcher _assetProxyDispatcher) public MixinExchangeCore() MixinSignatureValidator() - MixinSettlementProxy(_tokenTransferProxy, _zrxToken) + MixinSettlementProxy(_assetProxyDispatcher, _zrxToken, _zrxProxyMetadata) MixinWrapperFunctions() {} } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/IExchange.sol b/packages/contracts/src/contracts/current/protocol/Exchange/IExchange.sol index b7164a4e9..3315e270f 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/IExchange.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/IExchange.sol @@ -54,11 +54,6 @@ contract IExchange { bytes32 indexed orderHash ); - event LogCancelBefore( - address indexed maker, - uint256 salt - ); - function ZRX_TOKEN_CONTRACT() public view returns (address); diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/LibOrder.sol b/packages/contracts/src/contracts/current/protocol/Exchange/LibOrder.sol index 5562e692e..759619fc4 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/LibOrder.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/LibOrder.sol @@ -33,7 +33,9 @@ contract LibOrder { "uint256 makerFee", "uint256 takerFee", "uint256 expirationTimeSeconds", - "uint256 salt" + "uint256 salt", + "bytes makerAssetProxyData", + "bytes takerAssetProxyData" ); struct Order { @@ -48,6 +50,8 @@ contract LibOrder { uint256 takerFee; uint256 expirationTimeSeconds; uint256 salt; + bytes makerAssetProxyData; + bytes takerAssetProxyData; } /// @dev Calculates Keccak-256 hash of the order. @@ -73,7 +77,9 @@ contract LibOrder { order.makerFee, order.takerFee, order.expirationTimeSeconds, - order.salt + order.salt, + order.makerAssetProxyData, + order.takerAssetProxyData ) ); return orderHash; diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/LibPartialAmount.sol b/packages/contracts/src/contracts/current/protocol/Exchange/LibPartialAmount.sol index 0cf636840..2ff244e98 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/LibPartialAmount.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/LibPartialAmount.sol @@ -36,5 +36,3 @@ contract LibPartialAmount is SafeMath { return partialAmount; } } - - diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinSettlementProxy.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinSettlementProxy.sol index 2d7e98184..be5fec96a 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinSettlementProxy.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinSettlementProxy.sol @@ -20,22 +20,22 @@ pragma solidity ^0.4.21; pragma experimental ABIEncoderV2; import "./mixins/MSettlement.sol"; -import "../TokenTransferProxy/ITokenTransferProxy.sol"; import "../../tokens/Token/IToken.sol"; import "./LibPartialAmount.sol"; +import "../AssetProxyDispatcher/IAssetProxyDispatcher.sol"; /// @dev Provides MixinSettlement contract MixinSettlementProxy is MSettlement, LibPartialAmount { - - ITokenTransferProxy TRANSFER_PROXY; + IAssetProxyDispatcher TRANSFER_PROXY; + bytes ZRX_PROXY_METADATA; IToken ZRX_TOKEN; function transferProxy() - external view - returns (ITokenTransferProxy) + public view + returns (IAssetProxyDispatcher) { return TRANSFER_PROXY; } @@ -47,15 +47,26 @@ contract MixinSettlementProxy is return ZRX_TOKEN; } + function zrxProxyMetadata() + external view + returns (bytes) + { + return ZRX_PROXY_METADATA; + } + function MixinSettlementProxy( - ITokenTransferProxy _proxyContract, - IToken _zrxToken) + IAssetProxyDispatcher assetProxyDispatcherContract, + IToken zrxToken, + bytes zrxProxyMetadata) public { - ZRX_TOKEN = _zrxToken; - TRANSFER_PROXY = _proxyContract; + ZRX_TOKEN = zrxToken; + TRANSFER_PROXY = assetProxyDispatcherContract; + ZRX_PROXY_METADATA = zrxProxyMetadata; } + + function settleOrder( Order memory order, address takerAddress, @@ -68,43 +79,35 @@ contract MixinSettlementProxy is ) { makerTokenFilledAmount = getPartialAmount(takerTokenFilledAmount, order.takerTokenAmount, order.makerTokenAmount); - require( - TRANSFER_PROXY.transferFrom( - order.makerTokenAddress, - order.makerAddress, - takerAddress, - makerTokenFilledAmount - ) + TRANSFER_PROXY.transferFrom( + order.makerAssetProxyData, + order.makerAddress, + takerAddress, + makerTokenFilledAmount ); - require( - TRANSFER_PROXY.transferFrom( - order.takerTokenAddress, - takerAddress, - order.makerAddress, - takerTokenFilledAmount - ) + TRANSFER_PROXY.transferFrom( + order.takerAssetProxyData, + takerAddress, + order.makerAddress, + takerTokenFilledAmount ); if (order.feeRecipientAddress != address(0)) { if (order.makerFee > 0) { makerFeePaid = getPartialAmount(takerTokenFilledAmount, order.takerTokenAmount, order.makerFee); - require( - TRANSFER_PROXY.transferFrom( - ZRX_TOKEN, - order.makerAddress, - order.feeRecipientAddress, - makerFeePaid - ) + TRANSFER_PROXY.transferFrom( + ZRX_PROXY_METADATA, + order.makerAddress, + order.feeRecipientAddress, + makerFeePaid ); } if (order.takerFee > 0) { takerFeePaid = getPartialAmount(takerTokenFilledAmount, order.takerTokenAmount, order.takerFee); - require( - TRANSFER_PROXY.transferFrom( - ZRX_TOKEN, - takerAddress, - order.feeRecipientAddress, - takerFeePaid - ) + TRANSFER_PROXY.transferFrom( + ZRX_PROXY_METADATA, + takerAddress, + order.feeRecipientAddress, + takerFeePaid ); } } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol index 8f52043c4..fdc906076 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol @@ -65,76 +65,163 @@ contract MixinWrapperFunctions is // We need to call MExchangeCore.fillOrder using a delegatecall in // assembly so that we can intercept a call that throws. For this, we // need the input encoded in memory in the Ethereum ABIv2 format [1]. - - // | Offset | Length | Contents | - // |--------|---------|------------------------------| - // | 0 | 4 | function selector | - // | 4 | 11 * 32 | Order order | - // | 356 | 32 | uint256 takerTokenFillAmount | - // | 388 | 32 | offset to signature (416) | - // | 420 | 32 | len(signature) | - // | 452 | (1) | signature | - // | (2) | (3) | padding (zero) | - // | (4) | | end of input | - - // (1): len(signature) - // (2): 452 + len(signature) - // (3): (32 - len(signature)) mod 32 - // (4): 452 + len(signature) + (32 - len(signature)) mod 32 - + + // | Area | Offset | Length | Contents | + // | -------- |--------|---------|-------------------------------------------- | + // | Header | 0x00 | 4 | function selector | + // | Params | | 3 * 32 | function parameters: | + // | | 0x00 | | 1. offset to order (*) | + // | | 0x20 | | 2. takerTokenFillAmount | + // | | 0x40 | | 3. offset to signature (*) | + // | Data | | 13 * 32 | order: | + // | | 0x000 | | 1. makerAddress | + // | | 0x020 | | 2. takerAddress | + // | | 0x040 | | 3. makerTokenAddress | + // | | 0x060 | | 4. takerTokenAddress | + // | | 0x080 | | 5. feeRecipientAddress | + // | | 0x0A0 | | 6. makerTokenAmount | + // | | 0x0C0 | | 7. takerTokenAmount | + // | | 0x0E0 | | 8. makerFeeAmount | + // | | 0x100 | | 9. takerFeeAmount | + // | | 0x120 | | 10. expirationTimeSeconds | + // | | 0x140 | | 11. salt | + // | | 0x160 | | 12. Offset to makerAssetProxyMetadata (*) | + // | | 0x180 | | 13. Offset to takerAssetProxyMetadata (* | + // | | 0x1A0 | 32 | makerAssetProxyMetadata Length | + // | | 0x1C0 | ** | makerAssetProxyMetadata Contents | + // | | 0x1E0 | 32 | takerAssetProxyMetadata Length | + // | | 0x200 | ** | takerAssetProxyMetadata Contents | + // | | 0x220 | 32 | signature Length | + // | | 0x240 | ** | signature Contents | + + // * Offsets are calculated from the beginning of the current area: Header, Params, Data: + // An offset stored in the Params area is calculated from the beginning of the Params section. + // An offset stored in the Data area is calculated from the beginning of the Data section. + + // ** The length of dynamic array contents are stored in the field immediately preceeding the contents. + // [1]: https://solidity.readthedocs.io/en/develop/abi-spec.html bytes4 fillOrderSelector = this.fillOrder.selector; assembly { + + // Areas below may use the following variables: + // 1. Start -- Start of this area in memory + // 2. End -- End of this area in memory. This value may + // be precomputed (before writing contents), + // or it may be computed as contents are written. + // 3. Offset -- Current offset into area. If an area's End + // is precomputed, this variable tracks the + // offsets of contents as they are written. + + /////// Setup Header Area /////// // Load free memory pointer - let start := mload(0x40) - - // Write function signature - mstore(start, fillOrderSelector) - - // Write order struct - mstore(add(start, 4), mload(order)) // makerAddress - mstore(add(start, 36), mload(add(order, 32))) // takerAddress - mstore(add(start, 68), mload(add(order, 64))) // makerTokenAddress - mstore(add(start, 100), mload(add(order, 96))) // takerTokenAddress - mstore(add(start, 132), mload(add(order, 128))) // feeRecipientAddress - mstore(add(start, 164), mload(add(order, 160))) // makerTokenAmount - mstore(add(start, 196), mload(add(order, 192))) // takerTokenAmount - mstore(add(start, 228), mload(add(order, 224))) // makerFeeAmount - mstore(add(start, 260), mload(add(order, 256))) // takerFeeAmount - mstore(add(start, 292), mload(add(order, 288))) // expirationTimeSeconds - mstore(add(start, 324), mload(add(order, 320))) // salt - - // Write takerTokenFillAmount - mstore(add(start, 356), takerTokenFillAmount) - - // Write signature offset - mstore(add(start, 388), 416) - - // Write signature length - let sigLen := mload(signature) - mstore(add(start, 420), sigLen) - - // Calculate signature length with padding - let paddingLen := mod(sub(0, sigLen), 32) - let sigLenWithPadding := add(sigLen, paddingLen) - - // Write signature - let sigStart := add(signature, 32) - for { let curr := 0 } - lt(curr, sigLenWithPadding) - { curr := add(curr, 32) } - { mstore(add(start, add(452, curr)), mload(add(sigStart, curr))) } // Note: we assume that padding consists of only 0's + let headerAreaStart := mload(0x40) + mstore(headerAreaStart, fillOrderSelector) + let headerAreaEnd := add(headerAreaStart, 0x4) + + /////// Setup Params Area /////// + // This area is preallocated and written to later. + // This is because we need to fill in offsets that have not yet been calculated. + let paramsAreaStart := headerAreaEnd + let paramsAreaEnd := add(paramsAreaStart, 0x60) + let paramsAreaOffset := paramsAreaStart + + /////// Setup Data Area /////// + let dataAreaStart := paramsAreaEnd + let dataAreaEnd := dataAreaStart + + // Offset from the source data we're reading from + let sourceOffset := order + // bytesLen and bytesLenPadded track the length of a dynamically-allocated bytes array. + let bytesLen := 0 + let bytesLenPadded := 0 + + /////// Write order Struct /////// + // Write memory location of Order, relative to the start of the + // parameter list, then increment the paramsAreaOffset respectively. + mstore(paramsAreaOffset, sub(dataAreaEnd, paramsAreaStart)) + paramsAreaOffset := add(paramsAreaOffset, 0x20) + + // Write values for each field in the order + for{let i := 0} lt(i, 13) {i := add(i, 1)} { + mstore(dataAreaEnd, mload(sourceOffset)) + dataAreaEnd := add(dataAreaEnd, 0x20) + sourceOffset := add(sourceOffset, 0x20) + } + + // Write offset to + mstore(add(dataAreaStart, mul(11, 0x20)), sub(dataAreaEnd, dataAreaStart)) + + // Calculate length of + bytesLen := mload(sourceOffset) + sourceOffset := add(sourceOffset, 0x20) + bytesLenPadded := add(div(bytesLen, 0x20), gt(mod(bytesLen, 0x20), 0)) + + // Write length of + mstore(dataAreaEnd, bytesLen) + dataAreaEnd := add(dataAreaEnd, 0x20) + + // Write contents of + for {let i := 0} lt(i, bytesLenPadded) {i := add(i, 1)} { + mstore(dataAreaEnd, mload(sourceOffset)) + dataAreaEnd := add(dataAreaEnd, 0x20) + sourceOffset := add(sourceOffset, 0x20) + } + + // Write offset to + mstore(add(dataAreaStart, mul(12, 0x20)), sub(dataAreaEnd, dataAreaStart)) + + // Calculate length of + bytesLen := mload(sourceOffset) + sourceOffset := add(sourceOffset, 0x20) + bytesLenPadded := add(div(bytesLen, 0x20), gt(mod(bytesLen, 0x20), 0)) + + // Write length of + mstore(dataAreaEnd, bytesLen) + dataAreaEnd := add(dataAreaEnd, 0x20) + + // Write contents of + for {let i := 0} lt(i, bytesLenPadded) {i := add(i, 1)} { + mstore(dataAreaEnd, mload(sourceOffset)) + dataAreaEnd := add(dataAreaEnd, 0x20) + sourceOffset := add(sourceOffset, 0x20) + } + + /////// Write takerTokenFillAmount /////// + mstore(paramsAreaOffset, takerTokenFillAmount) + paramsAreaOffset := add(paramsAreaOffset, 0x20) + + /////// Write signature /////// + // Write offset to paramsArea + mstore(paramsAreaOffset, sub(dataAreaEnd, paramsAreaStart)) + + // Calculate length of signature + sourceOffset := signature + bytesLen := mload(sourceOffset) + sourceOffset := add(sourceOffset, 0x20) + bytesLenPadded := add(div(bytesLen, 0x20), gt(mod(bytesLen, 0x20), 0)) + + // Write length of signature + mstore(dataAreaEnd, bytesLen) + dataAreaEnd := add(dataAreaEnd, 0x20) + + // Write contents of signature + for {let i := 0} lt(i, bytesLenPadded) {i := add(i, 1)} { + mstore(dataAreaEnd, mload(sourceOffset)) + dataAreaEnd := add(dataAreaEnd, 0x20) + sourceOffset := add(sourceOffset, 0x20) + } // Execute delegatecall let success := delegatecall( - gas, // forward all gas, TODO: look into gas consumption of assert/throw - address, // call address of this contract - start, // pointer to start of input - add(452, sigLenWithPadding), // input length is 420 + signature length + padding length - start, // write output over input - 128 // output size is 128 bytes + gas, // forward all gas, TODO: look into gas consumption of assert/throw + address, // call address of this contract + headerAreaStart, // pointer to start of input + sub(dataAreaEnd, headerAreaStart), // length of input + headerAreaStart, // write output over input + 128 // output size is 128 bytes ) switch success case 0 { @@ -144,12 +231,11 @@ contract MixinWrapperFunctions is mstore(add(fillResults, 96), 0) } case 1 { - mstore(fillResults, mload(start)) - mstore(add(fillResults, 32), mload(add(start, 32))) - mstore(add(fillResults, 64), mload(add(start, 64))) - mstore(add(fillResults, 96), mload(add(start, 96))) + mstore(fillResults, mload(headerAreaStart)) + mstore(add(fillResults, 32), mload(add(headerAreaStart, 32))) + mstore(add(fillResults, 64), mload(add(headerAreaStart, 64))) + mstore(add(fillResults, 96), mload(add(headerAreaStart, 96))) } - } return fillResults; } @@ -228,10 +314,10 @@ contract MixinWrapperFunctions is // Token being sold by taker must be the same for each order require(orders[i].takerTokenAddress == orders[0].takerTokenAddress); - + // Calculate the remaining amount of takerToken to sell uint256 remainingTakerTokenFillAmount = safeSub(takerTokenFillAmount, totalFillResults.takerTokenFilledAmount); - + // Attempt to sell the remaining amount of takerToken FillResults memory singleFillResults = fillOrder( orders[i], @@ -270,7 +356,7 @@ contract MixinWrapperFunctions is // Calculate the remaining amount of takerToken to sell uint256 remainingTakerTokenFillAmount = safeSub(takerTokenFillAmount, totalFillResults.takerTokenFilledAmount); - + // Attempt to sell the remaining amount of takerToken FillResults memory singleFillResults = fillOrderNoThrow( orders[i], @@ -308,7 +394,7 @@ contract MixinWrapperFunctions is // Calculate the remaining amount of makerToken to buy uint256 remainingMakerTokenFillAmount = safeSub(makerTokenFillAmount, totalFillResults.makerTokenFilledAmount); - + // Convert the remaining amount of makerToken to buy into remaining amount // of takerToken to sell, assuming entire amount can be sold in the current order uint256 remainingTakerTokenFillAmount = getPartialAmount( @@ -405,5 +491,5 @@ contract MixinWrapperFunctions is totalFillResults.makerFeePaid = safeAdd(totalFillResults.makerFeePaid, singleFillResults.makerFeePaid); totalFillResults.takerFeePaid = safeAdd(totalFillResults.takerFeePaid, singleFillResults.takerFeePaid); } - + } diff --git a/packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol b/packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol new file mode 100644 index 000000000..61732a382 --- /dev/null +++ b/packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol @@ -0,0 +1,46 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.21; +import "/zeppelin/contracts/token/ERC721/ERC721Token.sol"; +import "../../utils/Ownable/Ownable.sol"; + +contract DummyERC721Token is + Ownable, + ERC721Token +{ + function DummyERC721Token( + string name, + string symbol) + public + ERC721Token(name, symbol) + {} + + /** + * @dev Internal function to mint a new token + * @dev Reverts if the given token ID already exists + * @param to address the beneficiary that will own the minted token + * @param tokenId uint256 ID of the token to be minted by the msg.sender + */ + function mint(address to, uint256 tokenId) + public + onlyOwner + { + super._mint(to, tokenId); + } +} diff --git a/packages/contracts/src/contracts/current/utils/Authorizable/Authorizable.sol b/packages/contracts/src/contracts/current/utils/Authorizable/Authorizable.sol new file mode 100644 index 000000000..cc27dd107 --- /dev/null +++ b/packages/contracts/src/contracts/current/utils/Authorizable/Authorizable.sol @@ -0,0 +1,109 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.21; + +import "./IAuthorizable.sol"; +import "../Ownable/Ownable.sol"; + +contract Authorizable is + Ownable, + IAuthorizable +{ + + /// @dev Only authorized addresses can invoke functions with this modifier. + modifier onlyAuthorized { + require(authorized[msg.sender]); + _; + } + + modifier targetAuthorized(address target) { + require(authorized[target]); + _; + } + + modifier targetNotAuthorized(address target) { + require(!authorized[target]); + _; + } + + mapping (address => bool) public authorized; + address[] public authorities; + + /* + * Public functions + */ + + /// @dev Authorizes an address. + /// @param target Address to authorize. + function addAuthorizedAddress(address target) + public + onlyOwner + targetNotAuthorized(target) + { + authorized[target] = true; + authorities.push(target); + emit LogAuthorizedAddressAdded(target, msg.sender); + } + + /// @dev Removes authorizion of an address. + /// @param target Address to remove authorization from. + function removeAuthorizedAddress(address target) + public + onlyOwner + targetAuthorized(target) + { + delete authorized[target]; + for (uint i = 0; i < authorities.length; i++) { + if (authorities[i] == target) { + authorities[i] = authorities[authorities.length - 1]; + authorities.length -= 1; + break; + } + } + emit LogAuthorizedAddressRemoved(target, msg.sender); + } + + /// @dev Removes authorizion of an address. + /// @param target Address to remove authorization from. + /// @param index Index of target in authorities array. + function removeAuthorizedAddressAtIndex(address target, uint256 index) + public + { + require(index < authorities.length); + require(authorities[index] == target); + delete authorized[target]; + authorities[index] = authorities[authorities.length - 1]; + authorities.length -= 1; + emit LogAuthorizedAddressRemoved(target, msg.sender); + } + + /* + * Public constant functions + */ + + /// @dev Gets all authorized addresses. + /// @return Array of authorized addresses. + function getAuthorizedAddresses() + public + constant + returns (address[]) + { + return authorities; + } +} diff --git a/packages/contracts/src/contracts/current/utils/Authorizable/IAuthorizable.sol b/packages/contracts/src/contracts/current/utils/Authorizable/IAuthorizable.sol new file mode 100644 index 000000000..903fc1667 --- /dev/null +++ b/packages/contracts/src/contracts/current/utils/Authorizable/IAuthorizable.sol @@ -0,0 +1,53 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.21; + +/// @title TokenTransferProxy - Transfers tokens on behalf of contracts that have been approved via decentralized governance. +contract IAuthorizable { + + /// @dev Gets all authorized addresses. + /// @return Array of authorized addresses. + function getAuthorizedAddresses() + public view + returns (address[]); + + /// @dev Authorizes an address. + /// @param target Address to authorize. + function addAuthorizedAddress(address target) + public; + + /// @dev Removes authorizion of an address. + /// @param target Address to remove authorization from. + function removeAuthorizedAddress(address target) + public; + + /// @dev Removes authorizion of an address. + /// @param target Address to remove authorization from. + /// @param index Index of target in authorities array. + function removeAuthorizedAddressAtIndex(address target, uint256 index) + public; + + event LogAuthorizedAddressAdded( + address indexed target, + address indexed caller); + + event LogAuthorizedAddressRemoved( + address indexed target, + address indexed caller); +} diff --git a/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol b/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol new file mode 100644 index 000000000..32a51d8ab --- /dev/null +++ b/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol @@ -0,0 +1,126 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.21; + +contract LibBytes { + + /// @dev Reads an address from a position in a byte array. + /// @param b Byte array containing an address. + /// @param index Index in byte array of address. + /// @return address from byte array. + function readAddress( + bytes b, + uint256 index) + public pure + returns (address result) + { + require(b.length >= index + 20); // 20 is length of address + + // Add offset to index: + // 1. Arrays are prefixed by 32-byte length parameter (add 32 to index) + // 2. Account for size difference between address length and 32-byte storage word (subtract 12 from index) + index += 20; + + // Read address from array memory + assembly { + // 1. Add index to to address of bytes array + // 2. Load 32-byte word from memory + // 3. Apply 20-byte mask to obtain address + result := and(mload(add(b, index)), 0xffffffffffffffffffffffffffffffffffffffff) + } + return result; + } + + /// @dev Writes an address into a specific position in a byte array. + /// @param input Address to put into byte array. + /// @param b Byte array to insert address into. + /// @param index Index in byte array of address. + function writeAddress( + address input, + bytes b, + uint256 index) + public pure + { + require(b.length >= index + 20); // 20 is length of address + + // Add offset to index: + // 1. Arrays are prefixed by 32-byte length parameter (add 32 to index) + // 2. Account for size difference between address length and 32-byte storage word (subtract 12 from index) + index += 20; + + // Store address into array memory + assembly { + // The address occupies 20 bytes and mstore stores 32 bytes. + // First fetch the 32-byte word where we'll be storing the address, then + // apply a mask so we have only the bytes in the word that the address will not occupy. + // Then combine these bytes with the address and store the 32 bytes back to memory with mstore. + + // 1. Add index to address of bytes array + // 2. Load 32-byte word from memory + // 3. Apply 12-byte mask to obtain extra bytes occupying word of memory where we'll store the address + let neighbors := and(mload(add(b, index)), 0xffffffffffffffffffffffff0000000000000000000000000000000000000000) + + // Store the neighbors and address into memory + mstore(add(b, index), xor(input, neighbors)) + } + } + + /// @dev Reads a uint256 value from a position in a byte array. + /// @param b Byte array containing a uint256 value. + /// @param index Index in byte array of uint256 value. + /// @return uint256 value from byte array. + function readUint256( + bytes b, + uint256 index) + public pure + returns (uint256 result) + { + require(b.length >= index + 32); + + // Arrays are prefixed by a 256 bit length parameter + index += 32; + + // Read the uint256 from array memory + assembly { + result := mload(add(b, index)) + } + return result; + } + + /// @dev Writes a uint256 into a specific position in a byte array. + /// @param input uint256 to put into byte array. + /// @param b Byte array to insert into. + /// @param index Index in byte array of . + function writeUint256( + uint256 input, + bytes b, + uint256 index) + public pure + { + require(b.length >= index + 32); + + // Arrays are prefixed by a 256 bit length parameter + index += 32; + + // Read the uint256 from array memory + assembly { + mstore(add(b, index), input) + } + } +} diff --git a/packages/contracts/src/contracts/current/utils/Ownable/IOwnable.sol b/packages/contracts/src/contracts/current/utils/Ownable/IOwnable.sol new file mode 100644 index 000000000..7784a7ba9 --- /dev/null +++ b/packages/contracts/src/contracts/current/utils/Ownable/IOwnable.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.4.21; + +/* + * Ownable + * + * Base contract with an owner. + * Provides onlyOwner modifier, which prevents function from running if it is called by anyone other than the owner. + */ + +contract IOwnable { + function transferOwnership(address newOwner) + public; +} diff --git a/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol b/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol index cb50f7252..91a5cb7ae 100644 --- a/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol +++ b/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol @@ -7,7 +7,9 @@ pragma solidity ^0.4.21; * Provides onlyOwner modifier, which prevents function from running if it is called by anyone other than the owner. */ -contract Ownable { +import "../Ownable/IOwnable.sol"; + +contract Ownable is IOwnable { address public owner; function Ownable() diff --git a/packages/contracts/src/utils/asset_proxy_utils.ts b/packages/contracts/src/utils/asset_proxy_utils.ts new file mode 100644 index 000000000..938110a75 --- /dev/null +++ b/packages/contracts/src/utils/asset_proxy_utils.ts @@ -0,0 +1,67 @@ +import { BigNumber } from '@0xproject/utils'; +import * as Web3 from 'web3'; + +import { AssetProxyId } from './types'; +const ethersUtils = require('ethers-utils'); + +export function zeroPad(value: string, width: number): string { + return '0'.repeat(width - value.length) + value; +} + +export function encodeAssetProxyId(assetProxyId: AssetProxyId, encoded_metadata: { value: string }) { + encoded_metadata.value += zeroPad(new BigNumber(assetProxyId).toString(16), 2); +} + +export function encodeAddress(address: string, encoded_metadata: { value: string }) { + encoded_metadata.value += zeroPad(address.replace('0x', ''), 40); +} + +export function encodeUint256(value: BigNumber, encoded_metadata: { value: string }) { + encoded_metadata.value += zeroPad(value.toString(16), 64); +} + +export function encodeERC20ProxyMetadata_V1(tokenAddress: string) { + // Encode metadata + const encoded_metadata = { value: '0x' }; + encodeAssetProxyId(AssetProxyId.ERC20_V1, encoded_metadata); + encodeAddress(tokenAddress, encoded_metadata); + + // Verify encoding length - '0x' plus 21 bytes of encoded data + if (encoded_metadata.value.length != 44) { + throw Error('Bad encoding length. Expected 44, got ' + encoded_metadata.value.length); + } + + // Return encoded metadata + return encoded_metadata.value; +} + +export function encodeERC20ProxyMetadata(tokenAddress: string) { + // Encode metadata + const encoded_metadata = { value: '0x' }; + encodeAssetProxyId(AssetProxyId.ERC20, encoded_metadata); + encodeAddress(tokenAddress, encoded_metadata); + + // Verify encoding length - '0x' plus 21 bytes of encoded data + if (encoded_metadata.value.length != 44) { + throw Error('Bad encoding length. Expected 44, got ' + encoded_metadata.value.length); + } + + // Return encoded metadata + return encoded_metadata.value; +} + +export function encodeERC721ProxyMetadata(tokenAddress: string, tokenId: BigNumber) { + // Encode metadata + const encoded_metadata = { value: '0x' }; + encodeAssetProxyId(AssetProxyId.ERC721, encoded_metadata); + encodeAddress(tokenAddress, encoded_metadata); + encodeUint256(tokenId, encoded_metadata); + + // Verify encoding length - '0x' plus 53 bytes of encoded data + if (encoded_metadata.value.length != 108) { + throw Error('Bad encoding length. Expected 108, got ' + encoded_metadata.value.length); + } + + // Return encoded metadata + return encoded_metadata.value; +} diff --git a/packages/contracts/src/utils/constants.ts b/packages/contracts/src/utils/constants.ts index d31e1e285..49872fc59 100644 --- a/packages/contracts/src/utils/constants.ts +++ b/packages/contracts/src/utils/constants.ts @@ -26,5 +26,6 @@ export const constants = { MAX_TOKEN_TRANSFERFROM_GAS: 80000, MAX_TOKEN_APPROVE_GAS: 60000, DUMMY_TOKEN_ARGS: [DUMMY_TOKEN_NAME, DUMMY_TOKEN_SYMBOL, DUMMY_TOKEN_DECIMALS, DUMMY_TOKEN_TOTAL_SUPPLY], + DUMMY_ERC721TOKEN_ARGS: [DUMMY_TOKEN_NAME, DUMMY_TOKEN_SYMBOL], TESTRPC_PRIVATE_KEYS: _.map(TESTRPC_PRIVATE_KEYS_STRINGS, privateKeyString => ethUtil.toBuffer(privateKeyString)), }; diff --git a/packages/contracts/src/utils/crypto.ts b/packages/contracts/src/utils/crypto.ts index 810072d2f..5bc678cdf 100644 --- a/packages/contracts/src/utils/crypto.ts +++ b/packages/contracts/src/utils/crypto.ts @@ -31,6 +31,8 @@ export const crypto = { argTypes.push('address'); } else if (_.isString(arg)) { argTypes.push('string'); + } else if (arg instanceof Buffer) { + argTypes.push('bytes'); } else if (_.isBoolean(arg)) { argTypes.push('bool'); } else { diff --git a/packages/contracts/src/utils/order_utils.ts b/packages/contracts/src/utils/order_utils.ts index 26336c81d..8eb4da35a 100644 --- a/packages/contracts/src/utils/order_utils.ts +++ b/packages/contracts/src/utils/order_utils.ts @@ -35,6 +35,8 @@ export const orderUtils = { takerFee: signedOrder.takerFee, expirationTimeSeconds: signedOrder.expirationTimeSeconds, salt: signedOrder.salt, + makerAssetProxyData: signedOrder.makerAssetProxyData, + takerAssetProxyData: signedOrder.takerAssetProxyData, }; return orderStruct; }, @@ -52,6 +54,8 @@ export const orderUtils = { 'uint256 takerFee', 'uint256 expirationTimeSeconds', 'uint256 salt', + 'bytes makerAssetProxyData', + 'bytes takerAssetProxyData', ]); const orderParamsHashBuff = crypto.solSHA3([ order.exchangeAddress, @@ -66,6 +70,8 @@ export const orderUtils = { order.takerFee, order.expirationTimeSeconds, order.salt, + ethUtil.toBuffer(order.makerAssetProxyData), + ethUtil.toBuffer(order.takerAssetProxyData), ]); const orderSchemaHashHex = `0x${orderSchemaHashBuff.toString('hex')}`; const orderParamsHashHex = `0x${orderParamsHashBuff.toString('hex')}`; diff --git a/packages/contracts/src/utils/types.ts b/packages/contracts/src/utils/types.ts index ed0ebeee9..f1636929b 100644 --- a/packages/contracts/src/utils/types.ts +++ b/packages/contracts/src/utils/types.ts @@ -37,6 +37,13 @@ export interface CancelOrdersBefore { salt: BigNumber; } +export enum AssetProxyId { + INVALID, + ERC20_V1, + ERC20, + ERC721, +} + export interface DefaultOrderParams { exchangeAddress: string; makerAddress: string; @@ -45,8 +52,10 @@ export interface DefaultOrderParams { takerTokenAddress: string; makerTokenAmount: BigNumber; takerTokenAmount: BigNumber; - makerFee: BigNumber; - takerFee: BigNumber; + makerFeeAmount: BigNumber; + takerFeeAmount: BigNumber; + makerAssetProxyData: string; + takerAssetProxyData: string; } export interface TransactionDataParams { @@ -100,6 +109,11 @@ export enum ContractName { AccountLevels = 'AccountLevels', EtherDelta = 'EtherDelta', Arbitrage = 'Arbitrage', + AssetProxyDispatcher = 'AssetProxyDispatcher', + ERC20TransferProxy = 'ERC20TransferProxy', + ERC20TransferProxy_V1 = 'ERC20TransferProxy_v1', + ERC721TransferProxy = 'ERC721TransferProxy', + DummyERC721Token = 'DummyERC721Token', } export interface Artifact { @@ -134,6 +148,8 @@ export interface OrderStruct { takerFee: BigNumber; expirationTimeSeconds: BigNumber; salt: BigNumber; + makerAssetProxyData: string; + takerAssetProxyData: string; } export interface UnsignedOrder extends OrderStruct { diff --git a/packages/contracts/test/asset_proxy_dispatcher/dispatcher.ts b/packages/contracts/test/asset_proxy_dispatcher/dispatcher.ts new file mode 100644 index 000000000..6cb9f2bc1 --- /dev/null +++ b/packages/contracts/test/asset_proxy_dispatcher/dispatcher.ts @@ -0,0 +1,339 @@ +import { LogWithDecodedArgs, TransactionReceiptWithDecodedLogs, ZeroEx } from '0x.js'; +import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import * as Web3 from 'web3'; + +import { AssetProxyDispatcherContract } from '../../src/contract_wrappers/generated/asset_proxy_dispatcher'; +import { DummyERC721TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c721_token'; +import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token'; +import { ERC20TransferProxyContract } from '../../src/contract_wrappers/generated/e_r_c20_transfer_proxy'; +import { ERC721TransferProxyContract } from '../../src/contract_wrappers/generated/e_r_c721_transfer_proxy'; +import { ERC20TransferProxy_v1Contract } from '../../src/contract_wrappers/generated/erc20transferproxy_v1'; +import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; +import { + encodeERC20ProxyMetadata, + encodeERC20ProxyMetadata_V1, + encodeERC721ProxyMetadata, +} from '../../src/utils/asset_proxy_utils'; +import { Balances } from '../../src/utils/balances'; +import { constants } from '../../src/utils/constants'; +import { AssetProxyId, ContractName } from '../../src/utils/types'; +import { chaiSetup } from '../utils/chai_setup'; +import { deployer } from '../utils/deployer'; +import { provider, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('AssetProxyDispatcher', () => { + let owner: string; + let notOwner: string; + let assetProxyManagerAddress: string; + let tokenOwner: string; + let makerAddress: string; + let takerAddress: string; + let zrx: DummyTokenContract; + let dmyBalances: Balances; + let tokenTransferProxy: TokenTransferProxyContract; + let assetProxyDispatcher: AssetProxyDispatcherContract; + let erc20TransferProxyV1: ERC20TransferProxy_v1Contract; + let erc20TransferProxy: ERC20TransferProxyContract; + let erc721TransferProxy: ERC721TransferProxyContract; + const nilAddress = '0x0000000000000000000000000000000000000000'; + const INITIAL_BALANCE = new BigNumber(10000); + + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owner = tokenOwner = accounts[0]; + notOwner = accounts[1]; + assetProxyManagerAddress = accounts[2]; + makerAddress = accounts[3]; + takerAddress = accounts[4]; + const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); + tokenTransferProxy = new TokenTransferProxyContract( + tokenTransferProxyInstance.abi, + tokenTransferProxyInstance.address, + provider, + ); + + const erc20TransferProxyV1Instance = await deployer.deployAsync(ContractName.ERC20TransferProxy_V1, [ + tokenTransferProxy.address, + ]); + erc20TransferProxyV1 = new ERC20TransferProxy_v1Contract( + erc20TransferProxyV1Instance.abi, + erc20TransferProxyV1Instance.address, + provider, + ); + + const erc20TransferProxyInstance = await deployer.deployAsync(ContractName.ERC20TransferProxy); + erc20TransferProxy = new ERC20TransferProxyContract( + erc20TransferProxyInstance.abi, + erc20TransferProxyInstance.address, + provider, + ); + + const erc721TransferProxyInstance = await deployer.deployAsync(ContractName.ERC721TransferProxy); + erc721TransferProxy = new ERC721TransferProxyContract( + erc721TransferProxyInstance.abi, + erc721TransferProxyInstance.address, + provider, + ); + + const assetProxyDispatcherInstance = await deployer.deployAsync(ContractName.AssetProxyDispatcher); + assetProxyDispatcher = new AssetProxyDispatcherContract( + assetProxyDispatcherInstance.abi, + assetProxyDispatcherInstance.address, + provider, + ); + + const zrxInstance = await deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS); + zrx = new DummyTokenContract(zrxInstance.abi, zrxInstance.address, provider); + await zrx.setBalance.sendTransactionAsync(makerAddress, INITIAL_BALANCE, { from: tokenOwner }); + await zrx.setBalance.sendTransactionAsync(takerAddress, INITIAL_BALANCE, { from: tokenOwner }); + dmyBalances = new Balances([zrx], [makerAddress, takerAddress]); + await zrx.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_BALANCE, { + from: takerAddress, + }); + await zrx.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_BALANCE, { + from: makerAddress, + }); + + await assetProxyDispatcher.addAuthorizedAddress.sendTransactionAsync(assetProxyManagerAddress, { + from: accounts[0], + }); + await erc20TransferProxyV1.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: accounts[0], + }); + await erc20TransferProxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: accounts[0], + }); + await erc721TransferProxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: accounts[0], + }); + await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(erc20TransferProxyV1.address, { + from: accounts[0], + }); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('setAssetProxy', () => { + it('should record proxy upon registration', async () => { + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: owner }, + ); + const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(erc20TransferProxy.address); + }); + + it('should be able to record multiple proxies', async () => { + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: owner }, + ); + let proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(erc20TransferProxy.address); + + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC721, + erc721TransferProxy.address, + nilAddress, + { from: owner }, + ); + proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC721); + expect(proxyAddress).to.be.equal(erc721TransferProxy.address); + }); + + it('should replace proxy address upon re-registration', async () => { + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: owner }, + ); + let proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(erc20TransferProxy.address); + + // Deploy a new version of the ERC20 Transfer Proxy contract + const newErc20TransferProxyInstance = await deployer.deployAsync(ContractName.ERC20TransferProxy); + const newErc20TransferProxy = new ERC20TransferProxyContract( + newErc20TransferProxyInstance.abi, + newErc20TransferProxyInstance.address, + provider, + ); + + const newAddress = newErc20TransferProxy.address; + const currentAddress = erc20TransferProxy.address; + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + newAddress, + currentAddress, + { from: owner }, + ); + proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(newAddress); + }); + + it('should throw if registering with incorrect "old_address" field', async () => { + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: owner }, + ); + const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(erc20TransferProxy.address); + + // The following transaction will throw because the currentAddress is no longer nilAddress + return expect( + assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: owner }, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should be able to reset proxy address to NULL', async () => { + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: owner }, + ); + const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(erc20TransferProxy.address); + + // The following transaction will reset the proxy address + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + nilAddress, + erc20TransferProxy.address, + { from: owner }, + ); + const newProxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(newProxyAddress).to.be.equal(nilAddress); + }); + + it('should throw if requesting address is not authorized', async () => { + return expect( + assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: notOwner }, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + }); + + describe('getAssetProxy', () => { + it('should return correct address of registered proxy', async () => { + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: owner }, + ); + const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(erc20TransferProxy.address); + }); + + it('should return NULL address if requesting non-existent proxy', async () => { + const proxyAddress = await assetProxyDispatcher.getAssetProxy.callAsync(AssetProxyId.ERC20); + expect(proxyAddress).to.be.equal(nilAddress); + }); + }); + + describe('transferFrom', () => { + it('should dispatch transfer to registered proxy', async () => { + // Register ERC20 proxy + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: owner }, + ); + + // Construct metadata for ERC20 proxy + const encodedProxyMetadata = encodeERC20ProxyMetadata(zrx.address); + + // Perform a transfer from makerAddress to takerAddress + const balances = await dmyBalances.getAsync(); + const amount = new BigNumber(10); + await assetProxyDispatcher.transferFrom.sendTransactionAsync( + encodedProxyMetadata, + makerAddress, + takerAddress, + amount, + { from: assetProxyManagerAddress }, + ); + + // Verify transfer was successful + const newBalances = await dmyBalances.getAsync(); + expect(newBalances[makerAddress][zrx.address]).to.be.bignumber.equal( + balances[makerAddress][zrx.address].minus(amount), + ); + expect(newBalances[takerAddress][zrx.address]).to.be.bignumber.equal( + balances[takerAddress][zrx.address].add(amount), + ); + }); + + it('should throw if delegating to unregistered proxy', async () => { + // Construct metadata for ERC20 proxy + const encodedProxyMetadata = encodeERC20ProxyMetadata(zrx.address); + + // Perform a transfer from makerAddress to takerAddress + const balances = await dmyBalances.getAsync(); + const amount = new BigNumber(10); + return expect( + assetProxyDispatcher.transferFrom.sendTransactionAsync( + encodedProxyMetadata, + makerAddress, + takerAddress, + amount, + { from: notOwner }, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw if requesting address is not authorized', async () => { + // Register ERC20 proxy + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: owner }, + ); + + // Construct metadata for ERC20 proxy + const encodedProxyMetadata = encodeERC20ProxyMetadata(zrx.address); + + // Perform a transfer from makerAddress to takerAddress + const balances = await dmyBalances.getAsync(); + const amount = new BigNumber(10); + return expect( + assetProxyDispatcher.transferFrom.sendTransactionAsync( + encodedProxyMetadata, + makerAddress, + takerAddress, + amount, + { from: notOwner }, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + }); +}); diff --git a/packages/contracts/test/asset_proxy_dispatcher/proxies.ts b/packages/contracts/test/asset_proxy_dispatcher/proxies.ts new file mode 100644 index 000000000..da1bb170c --- /dev/null +++ b/packages/contracts/test/asset_proxy_dispatcher/proxies.ts @@ -0,0 +1,407 @@ +import { LogWithDecodedArgs, TransactionReceiptWithDecodedLogs, ZeroEx } from '0x.js'; +import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import * as Web3 from 'web3'; + +import { AssetProxyDispatcherContract } from '../../src/contract_wrappers/generated/asset_proxy_dispatcher'; +import { DummyERC721TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c721_token'; +import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token'; +import { ERC20TransferProxyContract } from '../../src/contract_wrappers/generated/e_r_c20_transfer_proxy'; +import { ERC721TransferProxyContract } from '../../src/contract_wrappers/generated/e_r_c721_transfer_proxy'; +import { ERC20TransferProxy_v1Contract } from '../../src/contract_wrappers/generated/erc20transferproxy_v1'; +import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; +import { + encodeERC20ProxyMetadata, + encodeERC20ProxyMetadata_V1, + encodeERC721ProxyMetadata, +} from '../../src/utils/asset_proxy_utils'; +import { Balances } from '../../src/utils/balances'; +import { constants } from '../../src/utils/constants'; +import { AssetProxyId, ContractName } from '../../src/utils/types'; +import { chaiSetup } from '../utils/chai_setup'; +import { deployer } from '../utils/deployer'; +import { provider, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('Asset Transfer Proxies', () => { + let owner: string; + let notOwner: string; + let assetProxyManagerAddress: string; + let tokenOwner: string; + let makerAddress: string; + let takerAddress: string; + let zrx: DummyTokenContract; + let ck: DummyERC721TokenContract; + let dmyBalances: Balances; + let tokenTransferProxy: TokenTransferProxyContract; + let assetProxyDispatcher: AssetProxyDispatcherContract; + let erc20TransferProxyV1: ERC20TransferProxy_v1Contract; + let erc20TransferProxy: ERC20TransferProxyContract; + let erc721TransferProxy: ERC721TransferProxyContract; + const nilAddress = '0x0000000000000000000000000000000000000000'; + const makerTokenId = new BigNumber('0x1010101010101010101010101010101010101010101010101010101010101010'); + const INITIAL_BALANCE = new BigNumber(10000); + + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owner = tokenOwner = accounts[0]; + notOwner = accounts[1]; + assetProxyManagerAddress = accounts[2]; + makerAddress = accounts[3]; + takerAddress = accounts[4]; + const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); + tokenTransferProxy = new TokenTransferProxyContract( + tokenTransferProxyInstance.abi, + tokenTransferProxyInstance.address, + provider, + ); + + const erc20TransferProxyV1Instance = await deployer.deployAsync(ContractName.ERC20TransferProxy_V1, [ + tokenTransferProxy.address, + ]); + erc20TransferProxyV1 = new ERC20TransferProxy_v1Contract( + erc20TransferProxyV1Instance.abi, + erc20TransferProxyV1Instance.address, + provider, + ); + + const erc20TransferProxyInstance = await deployer.deployAsync(ContractName.ERC20TransferProxy); + erc20TransferProxy = new ERC20TransferProxyContract( + erc20TransferProxyInstance.abi, + erc20TransferProxyInstance.address, + provider, + ); + + const erc721TransferProxyInstance = await deployer.deployAsync(ContractName.ERC721TransferProxy); + erc721TransferProxy = new ERC721TransferProxyContract( + erc721TransferProxyInstance.abi, + erc721TransferProxyInstance.address, + provider, + ); + + const assetProxyDispatcherInstance = await deployer.deployAsync(ContractName.AssetProxyDispatcher); + assetProxyDispatcher = new AssetProxyDispatcherContract( + assetProxyDispatcherInstance.abi, + assetProxyDispatcherInstance.address, + provider, + ); + + const zrxInstance = await deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS); + zrx = new DummyTokenContract(zrxInstance.abi, zrxInstance.address, provider); + await zrx.setBalance.sendTransactionAsync(makerAddress, INITIAL_BALANCE, { from: tokenOwner }); + await zrx.setBalance.sendTransactionAsync(takerAddress, INITIAL_BALANCE, { from: tokenOwner }); + dmyBalances = new Balances([zrx], [makerAddress, takerAddress]); + await zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_BALANCE, { + from: takerAddress, + }); + await zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_BALANCE, { + from: makerAddress, + }); + await zrx.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_BALANCE, { + from: takerAddress, + }); + await zrx.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_BALANCE, { + from: makerAddress, + }); + + const ckInstance = await deployer.deployAsync(ContractName.DummyERC721Token, constants.DUMMY_ERC721TOKEN_ARGS); + ck = new DummyERC721TokenContract(ckInstance.abi, ckInstance.address, provider); + await ck.setApprovalForAll.sendTransactionAsync(erc721TransferProxy.address, true, { from: makerAddress }); + await ck.setApprovalForAll.sendTransactionAsync(erc721TransferProxy.address, true, { from: takerAddress }); + await ck.mint.sendTransactionAsync(makerAddress, makerTokenId, { from: tokenOwner }); + await assetProxyDispatcher.addAuthorizedAddress.sendTransactionAsync(assetProxyManagerAddress, { + from: accounts[0], + }); + await erc20TransferProxyV1.addAuthorizedAddress.sendTransactionAsync(assetProxyManagerAddress, { + from: accounts[0], + }); + await erc20TransferProxy.addAuthorizedAddress.sendTransactionAsync(assetProxyManagerAddress, { + from: accounts[0], + }); + await erc721TransferProxy.addAuthorizedAddress.sendTransactionAsync(assetProxyManagerAddress, { + from: accounts[0], + }); + await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(erc20TransferProxyV1.address, { + from: accounts[0], + }); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('Transfer Proxy - ERC20_V1', () => { + it('should successfully encode/decode metadata', async () => { + const metadata = await erc20TransferProxyV1.encodeMetadata.callAsync(AssetProxyId.ERC20_V1, zrx.address); + const address = await erc20TransferProxyV1.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(zrx.address); + }); + + it('should successfully decode metadata encoded by typescript helpers', async () => { + const metadata = encodeERC20ProxyMetadata_V1(zrx.address); + const address = await erc20TransferProxyV1.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(zrx.address); + }); + + it('should successfully encode/decode metadata padded with zeros', async () => { + const testAddress = '0x0000000000000000056000000000000000000010'; + const metadata = await erc20TransferProxyV1.encodeMetadata.callAsync(AssetProxyId.ERC20_V1, testAddress); + const address = await erc20TransferProxyV1.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(testAddress); + }); + + it('should successfully decode metadata encoded padded with zeros by typescript helpers', async () => { + const testAddress = '0x0000000000000000056000000000000000000010'; + const metadata = encodeERC20ProxyMetadata_V1(testAddress); + const address = await erc20TransferProxyV1.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(testAddress); + }); + + it('should successfully transfer tokens', async () => { + // Construct metadata for ERC20 proxy + const encodedProxyMetadata = encodeERC20ProxyMetadata_V1(zrx.address); + + // Perform a transfer from makerAddress to takerAddress + const balances = await dmyBalances.getAsync(); + const amount = new BigNumber(10); + await erc20TransferProxyV1.transferFrom.sendTransactionAsync( + encodedProxyMetadata, + makerAddress, + takerAddress, + amount, + { from: assetProxyManagerAddress }, + ); + + // Verify transfer was successful + const newBalances = await dmyBalances.getAsync(); + expect(newBalances[makerAddress][zrx.address]).to.be.bignumber.equal( + balances[makerAddress][zrx.address].minus(amount), + ); + expect(newBalances[takerAddress][zrx.address]).to.be.bignumber.equal( + balances[takerAddress][zrx.address].add(amount), + ); + }); + + it('should throw if requesting address is not authorized', async () => { + // Construct metadata for ERC20 proxy + const encodedProxyMetadata = encodeERC20ProxyMetadata_V1(zrx.address); + + // Perform a transfer from makerAddress to takerAddress + const balances = await dmyBalances.getAsync(); + const amount = new BigNumber(10); + expect( + erc20TransferProxyV1.transferFrom.sendTransactionAsync( + encodedProxyMetadata, + makerAddress, + takerAddress, + amount, + { from: notOwner }, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + }); + + describe('Transfer Proxy - ERC20', () => { + it('should successfully encode/decode metadata', async () => { + const metadata = await erc20TransferProxy.encodeMetadata.callAsync(AssetProxyId.ERC20, zrx.address); + const address = await erc20TransferProxy.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(zrx.address); + }); + + it('should successfully decode metadata encoded by typescript helpers', async () => { + const metadata = encodeERC20ProxyMetadata(zrx.address); + const address = await erc20TransferProxy.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(zrx.address); + }); + + it('should successfully encode/decode metadata padded with zeros', async () => { + const testAddress = '0x0000000000000000056000000000000000000010'; + const metadata = await erc20TransferProxy.encodeMetadata.callAsync(AssetProxyId.ERC20, testAddress); + const address = await erc20TransferProxy.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(testAddress); + }); + + it('should successfully decode metadata encoded padded with zeros by typescript helpers', async () => { + const testAddress = '0x0000000000000000056000000000000000000010'; + const metadata = encodeERC20ProxyMetadata(testAddress); + const address = await erc20TransferProxy.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(testAddress); + }); + + it('should successfully transfer tokens', async () => { + // Construct metadata for ERC20 proxy + const encodedProxyMetadata = encodeERC20ProxyMetadata(zrx.address); + + // Perform a transfer from makerAddress to takerAddress + const balances = await dmyBalances.getAsync(); + const amount = new BigNumber(10); + await erc20TransferProxy.transferFrom.sendTransactionAsync( + encodedProxyMetadata, + makerAddress, + takerAddress, + amount, + { from: assetProxyManagerAddress }, + ); + + // Verify transfer was successful + const newBalances = await dmyBalances.getAsync(); + expect(newBalances[makerAddress][zrx.address]).to.be.bignumber.equal( + balances[makerAddress][zrx.address].minus(amount), + ); + expect(newBalances[takerAddress][zrx.address]).to.be.bignumber.equal( + balances[takerAddress][zrx.address].add(amount), + ); + }); + + it('should throw if requesting address is not authorized', async () => { + // Construct metadata for ERC20 proxy + const encodedProxyMetadata = encodeERC20ProxyMetadata(zrx.address); + + // Perform a transfer from makerAddress to takerAddress + const balances = await dmyBalances.getAsync(); + const amount = new BigNumber(10); + expect( + erc20TransferProxy.transferFrom.sendTransactionAsync( + encodedProxyMetadata, + makerAddress, + takerAddress, + amount, + { from: notOwner }, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + }); + + describe('Transfer Proxy - ERC721', () => { + it('should successfully encode/decode metadata', async () => { + const metadata = await erc721TransferProxy.encodeMetadata.callAsync( + AssetProxyId.ERC721, + ck.address, + makerTokenId, + ); + const [address, tokenId] = await erc721TransferProxy.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(ck.address); + expect(tokenId).to.be.bignumber.equal(makerTokenId); + }); + + it('should successfully decode metadata encoded by typescript helpers', async () => { + const metadata = encodeERC721ProxyMetadata(ck.address, makerTokenId); + const [address, tokenId] = await erc721TransferProxy.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(ck.address); + expect(tokenId).to.be.bignumber.equal(makerTokenId); + }); + + it('should successfully encode/decode metadata padded with zeros', async () => { + const testAddress = '0x0000000000000000056000000000000000000010'; + const metadata = await erc721TransferProxy.encodeMetadata.callAsync( + AssetProxyId.ERC721, + testAddress, + makerTokenId, + ); + const [address, tokenId] = await erc721TransferProxy.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(testAddress); + expect(tokenId).to.be.bignumber.equal(makerTokenId); + }); + + it('should successfully decode metadata encoded padded with zeros by typescript helpers', async () => { + const testAddress = '0x0000000000000000056000000000000000000010'; + const metadata = encodeERC721ProxyMetadata(testAddress, makerTokenId); + const [address, tokenId] = await erc721TransferProxy.decodeMetadata.callAsync(metadata); + expect(address).to.be.equal(testAddress); + expect(tokenId).to.be.bignumber.equal(makerTokenId); + }); + + it('should successfully transfer tokens', async () => { + // Construct metadata for ERC20 proxy + const encodedProxyMetadata = encodeERC721ProxyMetadata(ck.address, makerTokenId); + + // Verify pre-condition + const ownerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(ownerMakerToken).to.be.bignumber.equal(makerAddress); + + // Perform a transfer from makerAddress to takerAddress + const balances = await dmyBalances.getAsync(); + const amount = new BigNumber(1); + await erc721TransferProxy.transferFrom.sendTransactionAsync( + encodedProxyMetadata, + makerAddress, + takerAddress, + amount, + { from: assetProxyManagerAddress }, + ); + + // Verify transfer was successful + const newOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(newOwnerMakerToken).to.be.bignumber.equal(takerAddress); + }); + + it('should throw if transferring 0 amount of a token', async () => { + // Construct metadata for ERC20 proxy + const encodedProxyMetadata = encodeERC721ProxyMetadata(ck.address, makerTokenId); + + // Verify pre-condition + const ownerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(ownerMakerToken).to.be.bignumber.equal(makerAddress); + + // Perform a transfer from makerAddress to takerAddress + const balances = await dmyBalances.getAsync(); + const amount = new BigNumber(0); + expect( + erc20TransferProxy.transferFrom.sendTransactionAsync( + encodedProxyMetadata, + makerAddress, + takerAddress, + amount, + { from: notOwner }, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw if transferring >1 amount of a token', async () => { + // Construct metadata for ERC20 proxy + const encodedProxyMetadata = encodeERC721ProxyMetadata(ck.address, makerTokenId); + + // Verify pre-condition + const ownerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(ownerMakerToken).to.be.bignumber.equal(makerAddress); + + // Perform a transfer from makerAddress to takerAddress + const balances = await dmyBalances.getAsync(); + const amount = new BigNumber(500); + expect( + erc20TransferProxy.transferFrom.sendTransactionAsync( + encodedProxyMetadata, + makerAddress, + takerAddress, + amount, + { from: notOwner }, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw if requesting address is not authorized', async () => { + // Construct metadata for ERC20 proxy + const encodedProxyMetadata = encodeERC721ProxyMetadata(zrx.address, makerTokenId); + + // Perform a transfer from makerAddress to takerAddress + const balances = await dmyBalances.getAsync(); + const amount = new BigNumber(1); + expect( + erc20TransferProxy.transferFrom.sendTransactionAsync( + encodedProxyMetadata, + makerAddress, + takerAddress, + amount, + { from: notOwner }, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + }); +}); diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index ef3b3b9ee..1277a88f3 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/packages/contracts/test/exchange/core.ts @@ -1,13 +1,18 @@ import { LogWithDecodedArgs, TransactionReceiptWithDecodedLogs, ZeroEx } from '0x.js'; - import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as chai from 'chai'; import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; import * as Web3 from 'web3'; +import { AssetProxyDispatcherContract } from '../../src/contract_wrappers/generated/asset_proxy_dispatcher'; +import { DummyERC721TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c721_token'; import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token'; +import { ERC20TransferProxyContract } from '../../src/contract_wrappers/generated/e_r_c20_transfer_proxy'; +import { ERC721TransferProxyContract } from '../../src/contract_wrappers/generated/e_r_c721_transfer_proxy'; +import { ERC20TransferProxy_v1Contract } from '../../src/contract_wrappers/generated/erc20transferproxy_v1'; import { CancelContractEventArgs, ExchangeContract, @@ -15,13 +20,25 @@ import { FillContractEventArgs, } from '../../src/contract_wrappers/generated/exchange'; import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; +import { + encodeERC20ProxyMetadata, + encodeERC20ProxyMetadata_V1, + encodeERC721ProxyMetadata, +} from '../../src/utils/asset_proxy_utils'; import { Balances } from '../../src/utils/balances'; import { constants } from '../../src/utils/constants'; import { crypto } from '../../src/utils/crypto'; import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; import { OrderFactory } from '../../src/utils/order_factory'; import { orderUtils } from '../../src/utils/order_utils'; -import { BalancesByOwner, ContractName, ExchangeContractErrs, SignatureType, SignedOrder } from '../../src/utils/types'; +import { + AssetProxyId, + BalancesByOwner, + ContractName, + ExchangeContractErrs, + SignatureType, + SignedOrder, +} from '../../src/utils/types'; import { chaiSetup } from '../utils/chai_setup'; import { deployer } from '../utils/deployer'; import { provider, web3Wrapper } from '../utils/web3_wrapper'; @@ -35,14 +52,21 @@ describe('Exchange', () => { let tokenOwner: string; let takerAddress: string; let feeRecipientAddress: string; + let assetProxyManagerAddress: string; const INITIAL_BALANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); const INITIAL_ALLOWANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); let rep: DummyTokenContract; let dgd: DummyTokenContract; let zrx: DummyTokenContract; + let ck: DummyERC721TokenContract; + let et: DummyERC721TokenContract; let exchange: ExchangeContract; let tokenTransferProxy: TokenTransferProxyContract; + let assetProxyDispatcher: AssetProxyDispatcherContract; + let erc20TransferProxyV1: ERC20TransferProxy_v1Contract; + let erc20TransferProxy: ERC20TransferProxyContract; + let erc721TransferProxy: ERC721TransferProxyContract; let signedOrder: SignedOrder; let balances: BalancesByOwner; @@ -50,32 +74,104 @@ describe('Exchange', () => { let dmyBalances: Balances; let orderFactory: OrderFactory; + let erc721TransferProxyInstance: Web3.ContractInstance; + let zeroEx: ZeroEx; before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); makerAddress = accounts[0]; - [tokenOwner, takerAddress, feeRecipientAddress] = accounts; - const [repInstance, dgdInstance, zrxInstance] = await Promise.all([ + [tokenOwner, takerAddress, feeRecipientAddress, assetProxyManagerAddress] = accounts; + const [repInstance, dgdInstance, zrxInstance, ckInstance, etInstance] = await Promise.all([ deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), + deployer.deployAsync(ContractName.DummyERC721Token, constants.DUMMY_ERC721TOKEN_ARGS), + deployer.deployAsync(ContractName.DummyERC721Token, constants.DUMMY_ERC721TOKEN_ARGS), ]); rep = new DummyTokenContract(repInstance.abi, repInstance.address, provider); dgd = new DummyTokenContract(dgdInstance.abi, dgdInstance.address, provider); zrx = new DummyTokenContract(zrxInstance.abi, zrxInstance.address, provider); + ck = new DummyERC721TokenContract(ckInstance.abi, ckInstance.address, provider); + et = new DummyERC721TokenContract(etInstance.abi, etInstance.address, provider); const tokenTransferProxyInstance = await deployer.deployAsync(ContractName.TokenTransferProxy); tokenTransferProxy = new TokenTransferProxyContract( tokenTransferProxyInstance.abi, tokenTransferProxyInstance.address, provider, ); + + const erc20TransferProxyV1Instance = await deployer.deployAsync(ContractName.ERC20TransferProxy_V1, [ + tokenTransferProxy.address, + ]); + erc20TransferProxyV1 = new ERC20TransferProxy_v1Contract( + erc20TransferProxyV1Instance.abi, + erc20TransferProxyV1Instance.address, + provider, + ); + + const erc20TransferProxyInstance = await deployer.deployAsync(ContractName.ERC20TransferProxy); + erc20TransferProxy = new ERC20TransferProxyContract( + erc20TransferProxyInstance.abi, + erc20TransferProxyInstance.address, + provider, + ); + + erc721TransferProxyInstance = await deployer.deployAsync(ContractName.ERC721TransferProxy); + erc721TransferProxy = new ERC721TransferProxyContract( + erc721TransferProxyInstance.abi, + erc721TransferProxyInstance.address, + provider, + ); + + const assetProxyDispatcherInstance = await deployer.deployAsync(ContractName.AssetProxyDispatcher); + assetProxyDispatcher = new AssetProxyDispatcherContract( + assetProxyDispatcherInstance.abi, + assetProxyDispatcherInstance.address, + provider, + ); + const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ zrx.address, - tokenTransferProxy.address, + encodeERC20ProxyMetadata(zrx.address), + assetProxyDispatcher.address, ]); exchange = new ExchangeContract(exchangeInstance.abi, exchangeInstance.address, provider); - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] }); + await assetProxyDispatcher.addAuthorizedAddress.sendTransactionAsync(assetProxyManagerAddress, { + from: accounts[0], + }); + await assetProxyDispatcher.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] }); + await erc20TransferProxyV1.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: accounts[0], + }); + await erc20TransferProxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: accounts[0], + }); + await erc721TransferProxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: accounts[0], + }); + await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(erc20TransferProxyV1.address, { + from: accounts[0], + }); + const nilAddress = '0x0000000000000000000000000000000000000000'; + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20_V1, + erc20TransferProxyV1.address, + nilAddress, + { from: accounts[0] }, + ); + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: accounts[0] }, + ); + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC721, + erc721TransferProxy.address, + nilAddress, + { from: accounts[0] }, + ); zeroEx = new ZeroEx(provider, { exchangeContractAddress: exchange.address, networkId: constants.TESTRPC_NETWORK_ID, @@ -92,6 +188,8 @@ describe('Exchange', () => { takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), makerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), takerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), + makerAssetProxyData: encodeERC20ProxyMetadata(rep.address), + takerAssetProxyData: encodeERC20ProxyMetadata(dgd.address), }; const privateKey = constants.TESTRPC_PRIVATE_KEYS[0]; orderFactory = new OrderFactory(privateKey, defaultOrderParams); @@ -103,6 +201,12 @@ describe('Exchange', () => { rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: takerAddress, }), + rep.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { + from: makerAddress, + }), + rep.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { + from: takerAddress, + }), rep.setBalance.sendTransactionAsync(makerAddress, INITIAL_BALANCE, { from: tokenOwner }), rep.setBalance.sendTransactionAsync(takerAddress, INITIAL_BALANCE, { from: tokenOwner }), dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { @@ -111,6 +215,12 @@ describe('Exchange', () => { dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: takerAddress, }), + dgd.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { + from: makerAddress, + }), + dgd.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { + from: takerAddress, + }), dgd.setBalance.sendTransactionAsync(makerAddress, INITIAL_BALANCE, { from: tokenOwner }), dgd.setBalance.sendTransactionAsync(takerAddress, INITIAL_BALANCE, { from: tokenOwner }), zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { @@ -119,8 +229,109 @@ describe('Exchange', () => { zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: takerAddress, }), + zrx.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { + from: makerAddress, + }), + zrx.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { + from: takerAddress, + }), zrx.setBalance.sendTransactionAsync(makerAddress, INITIAL_BALANCE, { from: tokenOwner }), zrx.setBalance.sendTransactionAsync(takerAddress, INITIAL_BALANCE, { from: tokenOwner }), + + // Distribute ck ERC721 tokens to maker & taker + // maker owns [0x1010.., ... , 0x4040..] and taker owns [0x5050.., ..., 0x9090..] + ck.setApprovalForAll.sendTransactionAsync(erc721TransferProxy.address, true, { from: makerAddress }), + ck.setApprovalForAll.sendTransactionAsync(erc721TransferProxy.address, true, { from: takerAddress }), + ck.mint.sendTransactionAsync( + makerAddress, + new BigNumber('0x1010101010101010101010101010101010101010101010101010101010101010'), + { from: tokenOwner }, + ), + ck.mint.sendTransactionAsync( + makerAddress, + new BigNumber('0x2020202020202020202020202020202020202020202020202020202020202020'), + { from: tokenOwner }, + ), + ck.mint.sendTransactionAsync( + makerAddress, + new BigNumber('0x3030303030303030303030303030303030303030303030303030303030303030'), + { from: tokenOwner }, + ), + ck.mint.sendTransactionAsync( + makerAddress, + new BigNumber('0x4040404040404040404040404040404040404040404040404040404040404040'), + { from: tokenOwner }, + ), + ck.mint.sendTransactionAsync( + takerAddress, + new BigNumber('0x5050505050505050505050505050505050505050505050505050505050505050'), + { from: tokenOwner }, + ), + ck.mint.sendTransactionAsync( + takerAddress, + new BigNumber('0x6060606060606060606060606060606060606060606060606060606060606060'), + { from: tokenOwner }, + ), + ck.mint.sendTransactionAsync( + takerAddress, + new BigNumber('0x7070707070707070707070707070707070707070707070707070707070707070'), + { from: tokenOwner }, + ), + ck.mint.sendTransactionAsync( + takerAddress, + new BigNumber('0x8080808080808080808080808080808080808080808080808080808080808080'), + { from: tokenOwner }, + ), + ck.mint.sendTransactionAsync( + takerAddress, + new BigNumber('0x9090909090909090909090909090909090909090909090909090909090909090'), + { from: tokenOwner }, + ), + + // Distribute et ERC721 tokens to maker & taker + // maker owns [0x1010.., ... , 0x4040..] and taker owns [0x5050.., ..., 0x9090..] + et.setApprovalForAll.sendTransactionAsync(erc721TransferProxy.address, true, { from: makerAddress }), + et.setApprovalForAll.sendTransactionAsync(erc721TransferProxy.address, true, { from: takerAddress }), + et.mint.sendTransactionAsync( + makerAddress, + new BigNumber('0x1010101010101010101010101010101010101010101010101010101010101010'), + { from: tokenOwner }, + ), + et.mint.sendTransactionAsync( + makerAddress, + new BigNumber('0x2020202020202020202020202020202020202020202020202020202020202020'), + { from: tokenOwner }, + ), + et.mint.sendTransactionAsync( + makerAddress, + new BigNumber('0x3030303030303030303030303030303030303030303030303030303030303030'), + { from: tokenOwner }, + ), + et.mint.sendTransactionAsync( + takerAddress, + new BigNumber('0x5050505050505050505050505050505050505050505050505050505050505050'), + { from: tokenOwner }, + ), + et.mint.sendTransactionAsync( + takerAddress, + new BigNumber('0x6060606060606060606060606060606060606060606060606060606060606060'), + { from: tokenOwner }, + ), + et.mint.sendTransactionAsync( + takerAddress, + new BigNumber('0x7070707070707070707070707070707070707070707070707070707070707070'), + { from: tokenOwner }, + ), + et.mint.sendTransactionAsync( + takerAddress, + new BigNumber('0x8080808080808080808080808080808080808080808080808080808080808080'), + { from: tokenOwner }, + ), + et.mint.sendTransactionAsync( + takerAddress, + new BigNumber('0x9090909090909090909090909090909090909090909090909090909090909090'), + { from: tokenOwner }, + ), ]); }); beforeEach(async () => { @@ -749,4 +960,284 @@ describe('Exchange', () => { ); }); }); + + describe('Testing Exchange of ERC721 Tokens', () => { + it('should successfully exchange a single token between the maker and taker (via fillOrder)', async () => { + // Construct Exchange parameters + const makerTokenId = new BigNumber('0x1010101010101010101010101010101010101010101010101010101010101010'); + const takerTokenId = new BigNumber('0x9090909090909090909090909090909090909090909090909090909090909090'); + signedOrder = orderFactory.newSignedOrder({ + makerTokenAddress: ck.address, + takerTokenAddress: ck.address, + makerTokenAmount: new BigNumber(1), + takerTokenAmount: new BigNumber(1), + makerAssetProxyData: encodeERC721ProxyMetadata(ck.address, makerTokenId), + takerAssetProxyData: encodeERC721ProxyMetadata(ck.address, takerTokenId), + }); + + // Verify pre-conditions + const initialOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(initialOwnerMakerToken).to.be.bignumber.equal(makerAddress); + const initialOwnerTakerToken = await ck.ownerOf.callAsync(takerTokenId); + expect(initialOwnerTakerToken).to.be.bignumber.equal(takerAddress); + + // Call Exchange + const takerTokenFillAmount = signedOrder.takerTokenAmount; + const res = await exWrapper.fillOrderAsync(signedOrder, takerAddress, { takerTokenFillAmount }); + + // Verify post-conditions + const newOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(newOwnerMakerToken).to.be.bignumber.equal(takerAddress); + const newOwnerTakerToken = await ck.ownerOf.callAsync(takerTokenId); + expect(newOwnerTakerToken).to.be.bignumber.equal(makerAddress); + }); + + it('should successfully exchange a single token between the maker and taker (via filleOrderNoThrow)', async () => { + // Construct Exchange parameters + const makerTokenId = new BigNumber('0x1010101010101010101010101010101010101010101010101010101010101010'); + const takerTokenId = new BigNumber('0x9090909090909090909090909090909090909090909090909090909090909090'); + signedOrder = orderFactory.newSignedOrder({ + makerTokenAddress: ck.address, + takerTokenAddress: ck.address, + makerTokenAmount: new BigNumber(1), + takerTokenAmount: new BigNumber(1), + makerAssetProxyData: encodeERC721ProxyMetadata(ck.address, makerTokenId), + takerAssetProxyData: encodeERC721ProxyMetadata(ck.address, takerTokenId), + }); + + // Verify pre-conditions + const initialOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(initialOwnerMakerToken).to.be.bignumber.equal(makerAddress); + const initialOwnerTakerToken = await ck.ownerOf.callAsync(takerTokenId); + expect(initialOwnerTakerToken).to.be.bignumber.equal(takerAddress); + + // Call Exchange + const takerTokenFillAmount = signedOrder.takerTokenAmount; + await exWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress, { takerTokenFillAmount }); + + // Verify post-conditions + const newOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(newOwnerMakerToken).to.be.bignumber.equal(takerAddress); + const newOwnerTakerToken = await ck.ownerOf.callAsync(takerTokenId); + expect(newOwnerTakerToken).to.be.bignumber.equal(makerAddress); + }); + + it('should throw when maker does not own the token with id makerTokenId', async () => { + // Construct Exchange parameters + const makerTokenId = new BigNumber('0x5050505050505050505050505050505050505050505050505050505050505050'); + const takerTokenId = new BigNumber('0x9090909090909090909090909090909090909090909090909090909090909090'); + signedOrder = orderFactory.newSignedOrder({ + makerTokenAddress: ck.address, + takerTokenAddress: ck.address, + makerTokenAmount: new BigNumber(1), + takerTokenAmount: new BigNumber(1), + makerAssetProxyData: encodeERC721ProxyMetadata(ck.address, makerTokenId), + takerAssetProxyData: encodeERC721ProxyMetadata(ck.address, takerTokenId), + }); + + // Verify pre-conditions + const initialOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(initialOwnerMakerToken).to.be.bignumber.not.equal(makerAddress); + const initialOwnerTakerToken = await ck.ownerOf.callAsync(takerTokenId); + expect(initialOwnerTakerToken).to.be.bignumber.equal(takerAddress); + + // Call Exchange + const takerTokenFillAmount = signedOrder.takerTokenAmount; + return expect( + exWrapper.fillOrderAsync(signedOrder, takerAddress, { takerTokenFillAmount }), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw when taker does not own the token with id takerTokenId', async () => { + // Construct Exchange parameters + const makerTokenId = new BigNumber('0x1010101010101010101010101010101010101010101010101010101010101010'); + const takerTokenId = new BigNumber('0x2020202020202020202020202020202020202020202020202020202020202020'); + signedOrder = orderFactory.newSignedOrder({ + makerTokenAddress: ck.address, + takerTokenAddress: ck.address, + makerTokenAmount: new BigNumber(1), + takerTokenAmount: new BigNumber(1), + makerAssetProxyData: encodeERC721ProxyMetadata(ck.address, makerTokenId), + takerAssetProxyData: encodeERC721ProxyMetadata(ck.address, takerTokenId), + }); + + // Verify pre-conditions + const initialOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(initialOwnerMakerToken).to.be.bignumber.equal(makerAddress); + const initialOwnerTakerToken = await ck.ownerOf.callAsync(takerTokenId); + expect(initialOwnerTakerToken).to.be.bignumber.not.equal(takerAddress); + + // Call Exchange + const takerTokenFillAmount = signedOrder.takerTokenAmount; + return expect( + exWrapper.fillOrderAsync(signedOrder, takerAddress, { takerTokenFillAmount }), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw when makerTokenAmount is greater than 1', async () => { + // Construct Exchange parameters + const makerTokenId = new BigNumber('0x1010101010101010101010101010101010101010101010101010101010101010'); + const takerTokenId = new BigNumber('0x9090909090909090909090909090909090909090909090909090909090909090'); + signedOrder = orderFactory.newSignedOrder({ + makerTokenAddress: ck.address, + takerTokenAddress: ck.address, + makerTokenAmount: new BigNumber(2), + takerTokenAmount: new BigNumber(1), + makerAssetProxyData: encodeERC721ProxyMetadata(ck.address, makerTokenId), + takerAssetProxyData: encodeERC721ProxyMetadata(ck.address, takerTokenId), + }); + + // Verify pre-conditions + const initialOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(initialOwnerMakerToken).to.be.bignumber.equal(makerAddress); + const initialOwnerTakerToken = await ck.ownerOf.callAsync(takerTokenId); + expect(initialOwnerTakerToken).to.be.bignumber.equal(takerAddress); + + // Call Exchange + const takerTokenFillAmount = signedOrder.takerTokenAmount; + return expect( + exWrapper.fillOrderAsync(signedOrder, takerAddress, { takerTokenFillAmount }), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw when takerTokenAmount is greater than 1', async () => { + // Construct Exchange parameters + const makerTokenId = new BigNumber('0x1010101010101010101010101010101010101010101010101010101010101010'); + const takerTokenId = new BigNumber('0x9090909090909090909090909090909090909090909090909090909090909090'); + signedOrder = orderFactory.newSignedOrder({ + makerTokenAddress: ck.address, + takerTokenAddress: ck.address, + makerTokenAmount: new BigNumber(1), + takerTokenAmount: new BigNumber(500), + makerAssetProxyData: encodeERC721ProxyMetadata(ck.address, makerTokenId), + takerAssetProxyData: encodeERC721ProxyMetadata(ck.address, takerTokenId), + }); + + // Verify pre-conditions + const initialOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(initialOwnerMakerToken).to.be.bignumber.equal(makerAddress); + const initialOwnerTakerToken = await ck.ownerOf.callAsync(takerTokenId); + expect(initialOwnerTakerToken).to.be.bignumber.equal(takerAddress); + + // Call Exchange + const takerTokenFillAmount = signedOrder.takerTokenAmount; + return expect( + exWrapper.fillOrderAsync(signedOrder, takerAddress, { takerTokenFillAmount }), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw on partial fill', async () => { + // Construct Exchange parameters + const makerTokenId = new BigNumber('0x1010101010101010101010101010101010101010101010101010101010101010'); + const takerTokenId = new BigNumber('0x9090909090909090909090909090909090909090909090909090909090909090'); + signedOrder = orderFactory.newSignedOrder({ + makerTokenAddress: ck.address, + takerTokenAddress: ck.address, + makerTokenAmount: new BigNumber(1), + takerTokenAmount: new BigNumber(0), + makerAssetProxyData: encodeERC721ProxyMetadata(ck.address, makerTokenId), + takerAssetProxyData: encodeERC721ProxyMetadata(ck.address, takerTokenId), + }); + + // Verify pre-conditions + const initialOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(initialOwnerMakerToken).to.be.bignumber.equal(makerAddress); + const initialOwnerTakerToken = await ck.ownerOf.callAsync(takerTokenId); + expect(initialOwnerTakerToken).to.be.bignumber.equal(takerAddress); + + // Call Exchange + const takerTokenFillAmount = signedOrder.takerTokenAmount; + return expect( + exWrapper.fillOrderAsync(signedOrder, takerAddress, { takerTokenFillAmount }), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should successfully fill order when makerToken is ERC721 and takerToken is ERC20', async () => { + // Construct Exchange parameters + const makerTokenId = new BigNumber('0x1010101010101010101010101010101010101010101010101010101010101010'); + signedOrder = orderFactory.newSignedOrder({ + makerTokenAddress: ck.address, + takerTokenAddress: dgd.address, + makerTokenAmount: new BigNumber(1), + takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + makerAssetProxyData: encodeERC721ProxyMetadata(ck.address, makerTokenId), + takerAssetProxyData: encodeERC20ProxyMetadata(dgd.address), + }); + + // Verify pre-conditions + const initialOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(initialOwnerMakerToken).to.be.bignumber.equal(makerAddress); + + // Call Exchange + balances = await dmyBalances.getAsync(); + const takerTokenFillAmount = signedOrder.takerTokenAmount; + await exWrapper.fillOrderAsync(signedOrder, takerAddress, { takerTokenFillAmount }); + + // Verify ERC721 token was transferred from Maker to Taker + const newOwnerMakerToken = await ck.ownerOf.callAsync(makerTokenId); + expect(newOwnerMakerToken).to.be.bignumber.equal(takerAddress); + + // Verify ERC20 tokens were transferred from Taker to Maker & fees were paid correctly + const newBalances = await dmyBalances.getAsync(); + expect(newBalances[makerAddress][signedOrder.takerTokenAddress]).to.be.bignumber.equal( + balances[makerAddress][signedOrder.takerTokenAddress].add(takerTokenFillAmount), + ); + expect(newBalances[takerAddress][signedOrder.takerTokenAddress]).to.be.bignumber.equal( + balances[takerAddress][signedOrder.takerTokenAddress].minus(takerTokenFillAmount), + ); + expect(newBalances[makerAddress][zrx.address]).to.be.bignumber.equal( + balances[makerAddress][zrx.address].minus(signedOrder.makerFee), + ); + expect(newBalances[takerAddress][zrx.address]).to.be.bignumber.equal( + balances[takerAddress][zrx.address].minus(signedOrder.takerFee), + ); + expect(newBalances[feeRecipientAddress][zrx.address]).to.be.bignumber.equal( + balances[feeRecipientAddress][zrx.address].add(signedOrder.makerFee.add(signedOrder.takerFee)), + ); + }); + + it('should successfully fill order when makerToken is ERC20 and takerToken is ERC721', async () => { + // Construct Exchange parameters + const takerTokenId = new BigNumber('0x9090909090909090909090909090909090909090909090909090909090909090'); + signedOrder = orderFactory.newSignedOrder({ + takerTokenAddress: ck.address, + makerTokenAddress: dgd.address, + takerTokenAmount: new BigNumber(1), + makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetProxyData: encodeERC721ProxyMetadata(ck.address, takerTokenId), + makerAssetProxyData: encodeERC20ProxyMetadata(dgd.address), + }); + + // Verify pre-conditions + const initialOwnerTakerToken = await ck.ownerOf.callAsync(takerTokenId); + expect(initialOwnerTakerToken).to.be.bignumber.equal(takerAddress); + + // Call Exchange + balances = await dmyBalances.getAsync(); + const takerTokenFillAmount = signedOrder.takerTokenAmount; + await exWrapper.fillOrderAsync(signedOrder, takerAddress, { takerTokenFillAmount }); + + // Verify ERC721 token was transferred from Taker to Maker + const newOwnerTakerToken = await ck.ownerOf.callAsync(takerTokenId); + expect(newOwnerTakerToken).to.be.bignumber.equal(makerAddress); + + // Verify ERC20 tokens were transferred from Maker to Taker & fees were paid correctly + const newBalances = await dmyBalances.getAsync(); + expect(newBalances[takerAddress][signedOrder.makerTokenAddress]).to.be.bignumber.equal( + balances[takerAddress][signedOrder.makerTokenAddress].add(signedOrder.makerTokenAmount), + ); + expect(newBalances[makerAddress][signedOrder.makerTokenAddress]).to.be.bignumber.equal( + balances[makerAddress][signedOrder.makerTokenAddress].minus(signedOrder.makerTokenAmount), + ); + expect(newBalances[makerAddress][zrx.address]).to.be.bignumber.equal( + balances[makerAddress][zrx.address].minus(signedOrder.makerFee), + ); + expect(newBalances[takerAddress][zrx.address]).to.be.bignumber.equal( + balances[takerAddress][zrx.address].minus(signedOrder.takerFee), + ); + expect(newBalances[feeRecipientAddress][zrx.address]).to.be.bignumber.equal( + balances[feeRecipientAddress][zrx.address].add(signedOrder.makerFee.add(signedOrder.takerFee)), + ); + }); + }); }); // tslint:disable-line:max-file-line-count diff --git a/packages/contracts/test/exchange/helpers.ts b/packages/contracts/test/exchange/helpers.ts index 4fa55efa1..37e53630e 100644 --- a/packages/contracts/test/exchange/helpers.ts +++ b/packages/contracts/test/exchange/helpers.ts @@ -6,11 +6,16 @@ import * as chai from 'chai'; import ethUtil = require('ethereumjs-util'); import { ExchangeContract } from '../../src/contract_wrappers/generated/exchange'; +import { + encodeERC20ProxyMetadata, + encodeERC20ProxyMetadata_V1, + encodeERC721ProxyMetadata, +} from '../../src/utils/asset_proxy_utils'; import { constants } from '../../src/utils/constants'; import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; import { OrderFactory } from '../../src/utils/order_factory'; import { orderUtils } from '../../src/utils/order_utils'; -import { ContractName, SignedOrder } from '../../src/utils/types'; +import { AssetProxyId, ContractName, SignedOrder } from '../../src/utils/types'; import { chaiSetup } from '../utils/chai_setup'; import { deployer } from '../utils/deployer'; import { provider, web3Wrapper } from '../utils/web3_wrapper'; @@ -23,6 +28,7 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); describe('Exchange', () => { let makerAddress: string; let feeRecipientAddress: string; + let assetProxyManagerAddress: string; let signedOrder: SignedOrder; let exchangeWrapper: ExchangeWrapper; @@ -30,9 +36,14 @@ describe('Exchange', () => { before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); - [makerAddress, feeRecipientAddress] = accounts; + [makerAddress, feeRecipientAddress, assetProxyManagerAddress] = accounts; const tokenRegistry = await deployer.deployAsync(ContractName.TokenRegistry); const tokenTransferProxy = await deployer.deployAsync(ContractName.TokenTransferProxy); + const assetProxyDispatcher = await deployer.deployAsync(ContractName.AssetProxyDispatcher); + const erc20TransferProxyV1 = await deployer.deployAsync(ContractName.ERC20TransferProxy_V1, [ + tokenTransferProxy.address, + ]); + const erc20TransferProxy = await deployer.deployAsync(ContractName.ERC20TransferProxy); const [rep, dgd, zrx] = await Promise.all([ deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), @@ -40,10 +51,36 @@ describe('Exchange', () => { ]); const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ zrx.address, - tokenTransferProxy.address, + AssetProxyId.ERC20, + assetProxyDispatcher.address, ]); const exchange = new ExchangeContract(exchangeInstance.abi, exchangeInstance.address, provider); - await tokenTransferProxy.addAuthorizedAddress(exchange.address, { from: accounts[0] }); + await assetProxyDispatcher.addAuthorizedAddress.sendTransactionAsync(assetProxyManagerAddress, { + from: accounts[0], + }); + await assetProxyDispatcher.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] }); + await erc20TransferProxyV1.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: accounts[0], + }); + await erc20TransferProxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: accounts[0], + }); + await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(erc20TransferProxyV1.address, { + from: accounts[0], + }); + const nilAddress = '0x0000000000000000000000000000000000000000'; + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20_V1, + erc20TransferProxyV1.address, + nilAddress, + { from: accounts[0] }, + ); + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: accounts[0] }, + ); const zeroEx = new ZeroEx(provider, { networkId: constants.TESTRPC_NETWORK_ID }); exchangeWrapper = new ExchangeWrapper(exchange, zeroEx); const defaultOrderParams = { @@ -56,6 +93,8 @@ describe('Exchange', () => { takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), makerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), takerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), + makerAssetProxyData: encodeERC20ProxyMetadata(rep.address), + takerAssetProxyData: encodeERC20ProxyMetadata(dgd.address), }; const privateKey = constants.TESTRPC_PRIVATE_KEYS[0]; orderFactory = new OrderFactory(privateKey, defaultOrderParams); diff --git a/packages/contracts/test/exchange/wrapper.ts b/packages/contracts/test/exchange/wrapper.ts index 0ba74e1e9..a58568adb 100644 --- a/packages/contracts/test/exchange/wrapper.ts +++ b/packages/contracts/test/exchange/wrapper.ts @@ -6,15 +6,23 @@ import * as chai from 'chai'; import * as _ from 'lodash'; import * as Web3 from 'web3'; +import { AssetProxyDispatcherContract } from '../../src/contract_wrappers/generated/asset_proxy_dispatcher'; import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token'; +import { ERC20TransferProxyContract } from '../../src/contract_wrappers/generated/e_r_c20_transfer_proxy'; +import { ERC20TransferProxy_v1Contract } from '../../src/contract_wrappers/generated/erc20transferproxy_v1'; import { ExchangeContract } from '../../src/contract_wrappers/generated/exchange'; import { TokenRegistryContract } from '../../src/contract_wrappers/generated/token_registry'; import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy'; +import { + encodeERC20ProxyMetadata, + encodeERC20ProxyMetadata_V1, + encodeERC721ProxyMetadata, +} from '../../src/utils/asset_proxy_utils'; import { Balances } from '../../src/utils/balances'; import { constants } from '../../src/utils/constants'; import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; import { OrderFactory } from '../../src/utils/order_factory'; -import { BalancesByOwner, ContractName, SignedOrder } from '../../src/utils/types'; +import { AssetProxyId, BalancesByOwner, ContractName, SignedOrder } from '../../src/utils/types'; import { chaiSetup } from '../utils/chai_setup'; import { deployer } from '../utils/deployer'; import { provider, web3Wrapper } from '../utils/web3_wrapper'; @@ -28,6 +36,7 @@ describe('Exchange', () => { let tokenOwner: string; let takerAddress: string; let feeRecipientAddress: string; + let assetProxyManagerAddress: string; const INITIAL_BALANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); const INITIAL_ALLOWANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18); @@ -38,6 +47,9 @@ describe('Exchange', () => { let exchange: ExchangeContract; let tokenRegistry: TokenRegistryContract; let tokenTransferProxy: TokenTransferProxyContract; + let assetProxyDispatcher: AssetProxyDispatcherContract; + let erc20TransferProxyV1: ERC20TransferProxy_v1Contract; + let erc20TransferProxy: ERC20TransferProxyContract; let balances: BalancesByOwner; @@ -48,7 +60,7 @@ describe('Exchange', () => { before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); tokenOwner = accounts[0]; - [makerAddress, takerAddress, feeRecipientAddress] = accounts; + [makerAddress, takerAddress, feeRecipientAddress, assetProxyManagerAddress] = accounts; const [repInstance, dgdInstance, zrxInstance] = await Promise.all([ deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), deployer.deployAsync(ContractName.DummyToken, constants.DUMMY_TOKEN_ARGS), @@ -65,12 +77,58 @@ describe('Exchange', () => { tokenTransferProxyInstance.address, provider, ); + const erc20TransferProxyV1Instance = await deployer.deployAsync(ContractName.ERC20TransferProxy_V1, [ + tokenTransferProxy.address, + ]); + erc20TransferProxyV1 = new ERC20TransferProxy_v1Contract( + erc20TransferProxyV1Instance.abi, + erc20TransferProxyV1Instance.address, + provider, + ); + const erc20TransferProxyInstance = await deployer.deployAsync(ContractName.ERC20TransferProxy); + erc20TransferProxy = new ERC20TransferProxyContract( + erc20TransferProxyInstance.abi, + erc20TransferProxyInstance.address, + provider, + ); + const assetProxyDispatcherInstance = await deployer.deployAsync(ContractName.AssetProxyDispatcher); + assetProxyDispatcher = new AssetProxyDispatcherContract( + assetProxyDispatcherInstance.abi, + assetProxyDispatcherInstance.address, + provider, + ); const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ zrx.address, - tokenTransferProxy.address, + encodeERC20ProxyMetadata(zrx.address), + assetProxyDispatcher.address, ]); exchange = new ExchangeContract(exchangeInstance.abi, exchangeInstance.address, provider); - await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] }); + await assetProxyDispatcher.addAuthorizedAddress.sendTransactionAsync(assetProxyManagerAddress, { + from: accounts[0], + }); + await assetProxyDispatcher.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] }); + await erc20TransferProxyV1.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: accounts[0], + }); + await erc20TransferProxy.addAuthorizedAddress.sendTransactionAsync(assetProxyDispatcher.address, { + from: accounts[0], + }); + await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(erc20TransferProxyV1.address, { + from: accounts[0], + }); + const nilAddress = '0x0000000000000000000000000000000000000000'; + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20_V1, + erc20TransferProxyV1.address, + nilAddress, + { from: accounts[0] }, + ); + await assetProxyDispatcher.setAssetProxy.sendTransactionAsync( + AssetProxyId.ERC20, + erc20TransferProxy.address, + nilAddress, + { from: accounts[0] }, + ); const zeroEx = new ZeroEx(provider, { networkId: constants.TESTRPC_NETWORK_ID }); exWrapper = new ExchangeWrapper(exchange, zeroEx); @@ -84,6 +142,8 @@ describe('Exchange', () => { takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), makerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), takerFee: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), + makerAssetProxyData: encodeERC20ProxyMetadata(rep.address), + takerAssetProxyData: encodeERC20ProxyMetadata(dgd.address), }; const privateKey = constants.TESTRPC_PRIVATE_KEYS[0]; @@ -92,14 +152,20 @@ describe('Exchange', () => { await Promise.all([ rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: makerAddress }), rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: takerAddress }), + rep.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { from: makerAddress }), + rep.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { from: takerAddress }), rep.setBalance.sendTransactionAsync(makerAddress, INITIAL_BALANCE, { from: tokenOwner }), rep.setBalance.sendTransactionAsync(takerAddress, INITIAL_BALANCE, { from: tokenOwner }), dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: makerAddress }), dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: takerAddress }), + dgd.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { from: makerAddress }), + dgd.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { from: takerAddress }), dgd.setBalance.sendTransactionAsync(makerAddress, INITIAL_BALANCE, { from: tokenOwner }), dgd.setBalance.sendTransactionAsync(takerAddress, INITIAL_BALANCE, { from: tokenOwner }), zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: makerAddress }), zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: takerAddress }), + zrx.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { from: makerAddress }), + zrx.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { from: takerAddress }), zrx.setBalance.sendTransactionAsync(makerAddress, INITIAL_BALANCE, { from: tokenOwner }), zrx.setBalance.sendTransactionAsync(takerAddress, INITIAL_BALANCE, { from: tokenOwner }), ]); @@ -246,11 +312,11 @@ describe('Exchange', () => { it('should not change balances if maker allowances are too low to fill order', async () => { const signedOrder = orderFactory.newSignedOrder(); - await rep.approve.sendTransactionAsync(tokenTransferProxy.address, new BigNumber(0), { + await rep.approve.sendTransactionAsync(erc20TransferProxy.address, new BigNumber(0), { from: makerAddress, }); await exWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); - await rep.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { + await rep.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { from: makerAddress, }); @@ -260,11 +326,11 @@ describe('Exchange', () => { it('should not change balances if taker allowances are too low to fill order', async () => { const signedOrder = orderFactory.newSignedOrder(); - await dgd.approve.sendTransactionAsync(tokenTransferProxy.address, new BigNumber(0), { + await dgd.approve.sendTransactionAsync(erc20TransferProxy.address, new BigNumber(0), { from: takerAddress, }); await exWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); - await dgd.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { + await dgd.approve.sendTransactionAsync(erc20TransferProxy.address, INITIAL_ALLOWANCE, { from: takerAddress, }); @@ -278,6 +344,7 @@ describe('Exchange', () => { makerTokenAddress: zrx.address, makerTokenAmount: makerZRXBalance, makerFee: new BigNumber(1), + makerAssetProxyData: encodeERC20ProxyMetadata(zrx.address), }); await exWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); const newBalances = await dmyBalances.getAsync(); @@ -285,11 +352,12 @@ describe('Exchange', () => { }); it('should not change balances if makerTokenAddress is ZRX, makerTokenAmount + makerFee > maker allowance', async () => { - const makerZRXAllowance = await zrx.allowance.callAsync(makerAddress, tokenTransferProxy.address); + const makerZRXAllowance = await zrx.allowance.callAsync(makerAddress, erc20TransferProxy.address); const signedOrder = orderFactory.newSignedOrder({ makerTokenAddress: zrx.address, makerTokenAmount: new BigNumber(makerZRXAllowance), makerFee: new BigNumber(1), + makerAssetProxyData: encodeERC20ProxyMetadata(zrx.address), }); await exWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); const newBalances = await dmyBalances.getAsync(); @@ -302,6 +370,7 @@ describe('Exchange', () => { takerTokenAddress: zrx.address, takerTokenAmount: takerZRXBalance, takerFee: new BigNumber(1), + takerAssetProxyData: encodeERC20ProxyMetadata(zrx.address), }); await exWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); const newBalances = await dmyBalances.getAsync(); @@ -309,11 +378,12 @@ describe('Exchange', () => { }); it('should not change balances if takerTokenAddress is ZRX, takerTokenAmount + takerFee > taker allowance', async () => { - const takerZRXAllowance = await zrx.allowance.callAsync(takerAddress, tokenTransferProxy.address); + const takerZRXAllowance = await zrx.allowance.callAsync(takerAddress, erc20TransferProxy.address); const signedOrder = orderFactory.newSignedOrder({ takerTokenAddress: zrx.address, takerTokenAmount: new BigNumber(takerZRXAllowance), takerFee: new BigNumber(1), + takerAssetProxyData: encodeERC20ProxyMetadata(zrx.address), }); await exWrapper.fillOrderNoThrowAsync(signedOrder, takerAddress); const newBalances = await dmyBalances.getAsync(); diff --git a/yarn.lock b/yarn.lock index 5075b03f9..78db47153 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1731,10 +1731,6 @@ buffer-from@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-0.1.2.tgz#15f4b9bcef012044df31142c14333caf6e0260d0" -buffer-from@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" - buffer-indexof@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" @@ -2313,10 +2309,9 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" concat-stream@^1.4.10, concat-stream@^1.5.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + version "1.6.1" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.1.tgz#261b8f518301f1d834e36342b9fea095d2620a26" dependencies: - buffer-from "^1.0.0" inherits "^2.0.3" readable-stream "^2.2.2" typedarray "^0.0.6" @@ -2570,8 +2565,8 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" core-js@^2.4.0, core-js@^2.5.0: - version "2.5.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.5.tgz#b14dde936c640c0579a6b50cabcc132dd6127e3b" + version "2.5.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -3955,8 +3950,8 @@ extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" external-editor@^2.0.4: - version "2.2.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" + version "2.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48" dependencies: chardet "^0.4.0" iconv-lite "^0.4.17" @@ -9402,14 +9397,14 @@ semver-sort@0.0.4: semver "^5.0.3" semver-regex "^1.0.0" -"semver@2 || 3 || 4 || 5", semver@5.5.0, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" - -semver@5.4.1, semver@~5.4.1: +"semver@2 || 3 || 4 || 5", semver@5.4.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@~5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" +semver@5.5.0, semver@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + semver@^4.1.0: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" @@ -10250,8 +10245,8 @@ swarm-js@0.1.37: xhr-request-promise "^0.1.2" symbol-observable@^1.0.3, symbol-observable@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + version "1.1.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32" symbol@~0.3.1: version "0.3.1" @@ -12006,7 +12001,7 @@ yauzl@^2.4.2: buffer-crc32 "~0.2.3" fd-slicer "~1.0.1" -zeppelin-solidity@1.8.0: +zeppelin-solidity@1.8.0, zeppelin-solidity@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/zeppelin-solidity/-/zeppelin-solidity-1.8.0.tgz#049fcde7daea9fc85210f8c6db9f8cd1ab8a853a" dependencies: -- cgit