diff options
author | Fabio Berger <me@fabioberger.com> | 2018-06-26 14:07:37 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2018-06-26 14:07:37 +0800 |
commit | e051743a3d9b92c8a5b275dc5843b98c37830b8c (patch) | |
tree | bcf434332365afe773ab5f6cef2e6e42356e8258 /packages | |
parent | f811c07454c87775f18374e8d955583dc157627f (diff) | |
parent | cba92a01b6dee208b497817445b7ae4048e299c0 (diff) | |
download | dexon-sol-tools-e051743a3d9b92c8a5b275dc5843b98c37830b8c.tar.gz dexon-sol-tools-e051743a3d9b92c8a5b275dc5843b98c37830b8c.tar.zst dexon-sol-tools-e051743a3d9b92c8a5b275dc5843b98c37830b8c.zip |
Merge branch 'v2-prototype' into refactor/check-revert-reasons
* v2-prototype: (21 commits)
Don't use variables for revert reasons
Add comments to dispatchTransferFrom
Document IAssetData
Compute bytes4 proxyid constants
Return inplace bytes in LibBytes.readBytesWithLength
Add Greg's documentation to MixinErc721Transfer
Add Greg's documentation to MixinErc20Transfer
Gas golf the transfer success logic
Optimize like in PR #767
Run prettier and fix linting errors
Fix tests
Update tests
Fix AssetProxyUtils
Update tests
Fix ERC721Proxy TokenId
Update AssetProxyUtils
Add IAssetData
WIP
ABI encoded asset data
Remove proxyId argument from dispatchTransferFrom
...
# Conflicts:
# packages/contracts/test/asset_proxy/proxies.ts
Diffstat (limited to 'packages')
36 files changed, 521 insertions, 364 deletions
diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 9c829c753..2e6376f39 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -64,11 +64,51 @@ yarn lint yarn test ``` -### Run Tests Against Geth +#### Testing options -Follow the instructions in the README for the devnet package to start the -devnet. +###### Revert stack traces + +If you want to see helpful stack traces (incl. line number, code snippet) for smart contract reverts, run the tests with: + +``` +yarn test:trace +``` + +**Note:** This currently slows down the test runs and is therefore not enabled by default. + +###### Backing Ethereum node + +By default, our tests run against an in-process [Ganache](https://github.com/trufflesuite/ganache-core) instance. In order to run the tests against [Geth](https://github.com/ethereum/go-ethereum), first follow the instructions in the README for the devnet package to start the devnet Geth node. Then run: ```bash TEST_PROVIDER=geth yarn test ``` + +###### Code coverage + +In order to see the Solidity code coverage output generated by `@0xproject/sol-cov`, run: + +``` +yarn test:coverage +``` + +###### Gas profiler + +In order to profile the gas costs for a specific smart contract call/transaction, you can run the tests in `profiler` mode. + +**Note:** Traces emitted by ganache have incorrect gas costs so we recommend using Geth for profiling. + +``` +TEST_PROVIDER=geth yarn test:profiler +``` + +You'll see a warning that you need to explicitly enable and disable the profiler before and after the block of code you want to profile. + +```typescript +import { profiler } from './utils/profiler'; +profiler.start(); +// Some call to a smart contract +profiler.stop(); +``` + +Without explicitly starting and stopping the profiler, the profiler output will be too busy, and therefore unusable. diff --git a/packages/contracts/compiler.json b/packages/contracts/compiler.json index 6b7b69caa..e1ea401b3 100644 --- a/packages/contracts/compiler.json +++ b/packages/contracts/compiler.json @@ -27,6 +27,7 @@ "ERC721Proxy", "Exchange", "ExchangeWrapper", + "IAssetData", "MixinAuthorizable", "MultiSigWallet", "MultiSigWalletWithTimeLock", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 8249b9e0c..95f2dbb4e 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -34,7 +34,7 @@ }, "config": { "abis": - "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Exchange|ExchangeWrapper|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetDataDecoders|TestAssetProxyDispatcher|TestLibBytes|TestLibs|TestSignatureValidator|TestValidator|TestWallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" + "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Exchange|ExchangeWrapper|IAssetData|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetDataDecoders|TestAssetProxyDispatcher|TestLibBytes|TestLibs|TestSignatureValidator|TestValidator|TestWallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" }, "repository": { "type": "git", diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol index 7ca823d1f..b74d2c231 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol @@ -30,14 +30,14 @@ contract ERC20Proxy is MixinERC20Transfer { // Id of this proxy. - uint8 constant PROXY_ID = 1; + bytes4 constant PROXY_ID = bytes4(keccak256("ERC20Token(address)")); /// @dev Gets the proxy id associated with the proxy address. /// @return Proxy id. function getProxyId() external view - returns (uint8) + returns (bytes4) { return PROXY_ID; } diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol index 7ff25aea3..f6293f97d 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol @@ -30,14 +30,14 @@ contract ERC721Proxy is MixinERC721Transfer { // Id of this proxy. - uint8 constant PROXY_ID = 2; + bytes4 constant PROXY_ID = bytes4(keccak256("ERC721Token(address,uint256,bytes)")); /// @dev Gets the proxy id associated with the proxy address. /// @return Proxy id. function getProxyId() external view - returns (uint8) + returns (bytes4) { return PROXY_ID; } diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAuthorizable.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAuthorizable.sol index 37c12f861..3b9584a44 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAuthorizable.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAuthorizable.sol @@ -19,12 +19,10 @@ pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; -import "./libs/LibAssetProxyErrors.sol"; import "../../utils/Ownable/Ownable.sol"; import "./mixins/MAuthorizable.sol"; contract MixinAuthorizable is - LibAssetProxyErrors, Ownable, MAuthorizable { @@ -33,7 +31,7 @@ contract MixinAuthorizable is modifier onlyAuthorized { require( authorized[msg.sender], - SENDER_NOT_AUTHORIZED + "SENDER_NOT_AUTHORIZED" ); _; } @@ -49,7 +47,7 @@ contract MixinAuthorizable is { require( !authorized[target], - TARGET_ALREADY_AUTHORIZED + "TARGET_ALREADY_AUTHORIZED" ); authorized[target] = true; @@ -65,7 +63,7 @@ contract MixinAuthorizable is { require( authorized[target], - TARGET_NOT_AUTHORIZED + "TARGET_NOT_AUTHORIZED" ); delete authorized[target]; @@ -91,15 +89,15 @@ contract MixinAuthorizable is { require( authorized[target], - TARGET_NOT_AUTHORIZED + "TARGET_NOT_AUTHORIZED" ); require( index < authorities.length, - INDEX_OUT_OF_BOUNDS + "INDEX_OUT_OF_BOUNDS" ); require( authorities[index] == target, - AUTHORIZED_ADDRESS_MISMATCH + "AUTHORIZED_ADDRESS_MISMATCH" ); delete authorized[target]; diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinERC20Transfer.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinERC20Transfer.sol index 0e7f3fc89..a09db43bd 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinERC20Transfer.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinERC20Transfer.sol @@ -21,11 +21,9 @@ pragma experimental ABIEncoderV2; import "../../utils/LibBytes/LibBytes.sol"; import "../../tokens/ERC20Token/IERC20Token.sol"; -import "./libs/LibTransferErrors.sol"; -contract MixinERC20Transfer is - LibTransferErrors -{ +contract MixinERC20Transfer { + using LibBytes for bytes; /// @dev Internal version of `transferFrom`. @@ -42,41 +40,74 @@ contract MixinERC20Transfer is internal { // Decode asset data. - address token = assetData.readAddress(0); - + address token = assetData.readAddress(16); + // Transfer tokens. // We do a raw call so we can check the success separate // from the return data. - bool success = token.call(abi.encodeWithSelector( - IERC20Token(token).transferFrom.selector, - from, - to, - amount - )); - require( - success, - TRANSFER_FAILED - ); + // We construct calldata for the `token.transferFrom` ABI. + // The layout of this calldata is in the table below. + // + // | Area | Offset | Length | Contents | + // |----------|--------|---------|-------------------------------------| + // | Header | 0 | 4 | function selector | + // | Params | | 3 * 32 | function parameters: | + // | | 4 | | 1. from | + // | | 36 | | 2. to | + // | | 68 | | 3. amount | - // Check return data. - // If there is no return data, we assume the token incorrectly - // does not return a bool. In this case we expect it to revert - // on failure, which was handled above. - // If the token does return data, we require that it is a single - // value that evaluates to true. + bytes4 transferFromSelector = IERC20Token(token).transferFrom.selector; + bool success; assembly { - if returndatasize { - success := 0 - if eq(returndatasize, 32) { - // First 64 bytes of memory are reserved scratch space - returndatacopy(0, 0, 32) - success := mload(0) - } - } + /////// Setup State /////// + // `cdStart` is the start of the calldata for `token.transferFrom` (equal to free memory ptr). + let cdStart := mload(64) + + /////// Setup Header Area /////// + // This area holds the 4-byte `transferFromSelector`. + // Any trailing data in transferFromSelector will be + // overwritten in the next `mstore` call. + mstore(cdStart, transferFromSelector) + + /////// Setup Params Area /////// + // Each parameter is padded to 32-bytes. + // The entire Params Area is 96 bytes. + // A 20-byte mask is applied to addresses to + // zero-out the unused bytes. + mstore(add(cdStart, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) + mstore(add(cdStart, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) + mstore(add(cdStart, 68), amount) + + /////// Call `token.transferFrom` using the calldata /////// + success := call( + gas, // forward all gas + token, // call address of token contract + 0, // don't send any ETH + cdStart, // pointer to start of input + 100, // length of input + cdStart, // write output over input + 32 // output size should be 32 bytes + ) + + /////// Check return data. /////// + // If there is no return data, we assume the token incorrectly + // does not return a bool. In this case we expect it to revert + // on failure, which was handled above. + // If the token does return data, we require that it is a single + // nonzero 32 bytes value. + // So the transfer succeeded if the call succeeded and either + // returned nothing, or returned a non-zero 32 byte value. + success := and(success, or( + iszero(returndatasize), + and( + eq(returndatasize, 32), + gt(mload(cdStart), 0) + ) + )) } require( success, - TRANSFER_FAILED + "TRANSFER_FAILED" ); } } diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinERC721Transfer.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinERC721Transfer.sol index 944068bbb..c170917ea 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinERC721Transfer.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinERC721Transfer.sol @@ -28,6 +28,8 @@ contract MixinERC721Transfer is { using LibBytes for bytes; + bytes4 constant SAFE_TRANSFER_FROM_SELECTOR = bytes4(keccak256("safeTransferFrom(address,address,uint256,bytes)")); + /// @dev Internal version of `transferFrom`. /// @param assetData Encoded byte array. /// @param from Address to transfer asset from. @@ -54,11 +56,85 @@ contract MixinERC721Transfer is bytes memory receiverData ) = decodeERC721AssetData(assetData); - ERC721Token(token).safeTransferFrom( - from, - to, - tokenId, - receiverData + // We construct calldata for the `token.safeTransferFrom` ABI. + // The layout of this calldata is in the table below. + // + // | Area | Offset | Length | Contents | + // |----------|--------|---------|-------------------------------------| + // | Header | 0 | 4 | function selector | + // | Params | | 4 * 32 | function parameters: | + // | | 4 | | 1. from | + // | | 36 | | 2. to | + // | | 68 | | 3. tokenId | + // | | 100 | | 4. offset to receiverData (*) | + // | Data | | | receiverData: | + // | | 132 | 32 | receiverData Length | + // | | 164 | ** | receiverData Contents | + + bytes4 safeTransferFromSelector = SAFE_TRANSFER_FROM_SELECTOR; + bool success; + assembly { + /////// Setup State /////// + // `cdStart` is the start of the calldata for + // `token.safeTransferFrom` (equal to free memory ptr). + let cdStart := mload(64) + // `dataAreaLength` is the total number of words + // needed to store `receiverData` + // As-per the ABI spec, this value is padded up to + // the nearest multiple of 32, + // and includes 32-bytes for length. + // It's calculated as folows: + // - Unpadded length in bytes = `mload(receiverData) + 32` + // - Add 31 to convert rounding down to rounding up. + // Combined with the previous and this is `63`. + // - Round down to nearest multiple of 32 by clearing + // bits 0x1F. This is done with `and` and a mask. + let dataAreaLength := and(add(mload(receiverData), 63), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0) + // `cdEnd` is the end of the calldata for `token.safeTransferFrom`. + let cdEnd := add(cdStart, add(132, dataAreaLength)) + + /////// Setup Header Area /////// + // This area holds the 4-byte `transferFromSelector`. + // Any trailing data in transferFromSelector will be + // overwritten in the next `mstore` call. + mstore(cdStart, safeTransferFromSelector) + + /////// Setup Params Area /////// + // Each parameter is padded to 32-bytes. + // The entire Params Area is 128 bytes. + // Notes: + // 1. A 20-byte mask is applied to addresses + // to zero-out the unused bytes. + // 2. The offset to `receiverData` is the length + // of the Params Area (128 bytes). + mstore(add(cdStart, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) + mstore(add(cdStart, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) + mstore(add(cdStart, 68), tokenId) + mstore(add(cdStart, 100), 128) + + /////// Setup Data Area /////// + // This area holds `receiverData`. + let dataArea := add(cdStart, 132) + for {} lt(dataArea, cdEnd) {} { + mstore(dataArea, mload(receiverData)) + dataArea := add(dataArea, 32) + receiverData := add(receiverData, 32) + } + + /////// Call `token.safeTransferFrom` using the calldata /////// + success := call( + gas, // forward all gas + token, // call address of token contract + 0, // don't send any ETH + cdStart, // pointer to start of input + sub(cdEnd, cdStart), // length of input + cdStart, // write output over input + 0 // output size is 0 bytes + ) + } + require( + success, + TRANSFER_FAILED ); } @@ -79,11 +155,9 @@ contract MixinERC721Transfer is ) { // Decode asset data. - token = assetData.readAddress(0); - tokenId = assetData.readUint256(20); - if (assetData.length > 52) { - receiverData = assetData.readBytesWithLength(52); - } + token = assetData.readAddress(16); + tokenId = assetData.readUint256(36); + receiverData = assetData.readBytesWithLength(100); return ( token, diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAssetData.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAssetData.sol new file mode 100644 index 000000000..8c78ee8c4 --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAssetData.sol @@ -0,0 +1,36 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.23; + +// @dev Interface of the asset proxy's assetData. +// The asset proxies take an ABI encoded `bytes assetData` as argument. +// This argument is ABI encoded as one of the methods of this interface. +interface IAssetData { + + function ERC20Token( + address tokenContract) + external pure; + + function ERC721Token( + address tokenContract, + uint256 tokenId, + bytes receiverData) + external pure; + +} diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAssetProxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAssetProxy.sol index 22f43b12f..d12d23610 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAssetProxy.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAssetProxy.sol @@ -56,6 +56,5 @@ contract IAssetProxy is function getProxyId() external view - returns (uint8); + returns (bytes4); } - diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/libs/LibAssetProxyErrors.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/libs/LibAssetProxyErrors.sol index dca4f400f..b0b20c044 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/libs/LibAssetProxyErrors.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/libs/LibAssetProxyErrors.sol @@ -18,7 +18,10 @@ pragma solidity ^0.4.24; +/// @dev This contract documents the revert reasons used in the AssetProxy contracts. +/// This contract is intended to serve as a reference, but is not actually used for efficiency reasons. contract LibAssetProxyErrors { + /// Authorizable errors /// string constant SENDER_NOT_AUTHORIZED = "SENDER_NOT_AUTHORIZED"; // Sender not authorized to call this method. string constant TARGET_NOT_AUTHORIZED = "TARGET_NOT_AUTHORIZED"; // Target address not authorized to call this method. diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/libs/LibTransferErrors.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/libs/LibTransferErrors.sol index ba784ab22..88187f196 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/libs/LibTransferErrors.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/libs/LibTransferErrors.sol @@ -18,7 +18,10 @@ pragma solidity ^0.4.24; +/// @dev This contract documents the revert reasons used in the `transferFrom` methods of different AssetProxy contracts. +/// This contract is intended to serve as a reference, but is not actually used for efficiency reasons. contract LibTransferErrors { + /// Transfer errors /// string constant INVALID_AMOUNT = "INVALID_AMOUNT"; // Transfer amount must equal 1. string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Transfer failed. diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinAssetProxyDispatcher.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinAssetProxyDispatcher.sol index b8d6c0722..3203322aa 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinAssetProxyDispatcher.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinAssetProxyDispatcher.sol @@ -19,17 +19,18 @@ pragma solidity ^0.4.24; import "../../utils/Ownable/Ownable.sol"; -import "./libs/LibExchangeErrors.sol"; +import "../../utils/LibBytes/LibBytes.sol"; import "./mixins/MAssetProxyDispatcher.sol"; import "../AssetProxy/interfaces/IAssetProxy.sol"; contract MixinAssetProxyDispatcher is Ownable, - LibExchangeErrors, MAssetProxyDispatcher { + using LibBytes for bytes; + // Mapping from Asset Proxy Id's to their respective Asset Proxy - mapping (uint8 => IAssetProxy) public assetProxies; + mapping (bytes4 => IAssetProxy) public assetProxies; /// @dev Registers an asset proxy to an asset proxy id. /// An id can only be assigned to a single proxy at a given time. @@ -37,7 +38,7 @@ contract MixinAssetProxyDispatcher is /// @param newAssetProxy Address of new asset proxy to register, or 0x0 to unset assetProxyId. /// @param oldAssetProxy Existing asset proxy to overwrite, or 0x0 if assetProxyId is currently unused. function registerAssetProxy( - uint8 assetProxyId, + bytes4 assetProxyId, address newAssetProxy, address oldAssetProxy ) @@ -48,17 +49,17 @@ contract MixinAssetProxyDispatcher is address currentAssetProxy = assetProxies[assetProxyId]; require( oldAssetProxy == currentAssetProxy, - ASSET_PROXY_MISMATCH + "ASSET_PROXY_MISMATCH" ); IAssetProxy assetProxy = IAssetProxy(newAssetProxy); // Ensure that the id of newAssetProxy matches the passed in assetProxyId, unless it is being reset to 0. if (newAssetProxy != address(0)) { - uint8 newAssetProxyId = assetProxy.getProxyId(); + bytes4 newAssetProxyId = assetProxy.getProxyId(); require( newAssetProxyId == assetProxyId, - ASSET_PROXY_ID_MISMATCH + "ASSET_PROXY_ID_MISMATCH" ); } @@ -74,7 +75,7 @@ contract MixinAssetProxyDispatcher is /// @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) + function getAssetProxy(bytes4 assetProxyId) external view returns (address) @@ -83,14 +84,12 @@ contract MixinAssetProxyDispatcher is } /// @dev Forwards arguments to assetProxy and calls `transferFrom`. Either succeeds or throws. - /// @param assetData Byte array encoded for the respective asset proxy. - /// @param assetProxyId Id of assetProxy to dispach to. + /// @param assetData Byte array encoded for the asset. /// @param from Address to transfer token from. /// @param to Address to transfer token to. /// @param amount Amount of token to transfer. function dispatchTransferFrom( bytes memory assetData, - uint8 assetProxyId, address from, address to, uint256 amount @@ -99,19 +98,94 @@ contract MixinAssetProxyDispatcher is { // Do nothing if no amount should be transferred. if (amount > 0) { + // Ensure assetData length is valid + require( + assetData.length > 3, + "LENGTH_GREATER_THAN_3_REQUIRED" + ); + // Lookup assetProxy + bytes4 assetProxyId; + assembly { + assetProxyId := and(mload( + add(assetData, 32)), + 0xFFFFFFFF00000000000000000000000000000000000000000000000000000000 + ) + } IAssetProxy assetProxy = assetProxies[assetProxyId]; + // Ensure that assetProxy exists require( assetProxy != address(0), - ASSET_PROXY_DOES_NOT_EXIST + "ASSET_PROXY_DOES_NOT_EXIST" ); - // transferFrom will either succeed or throw. - assetProxy.transferFrom( - assetData, - from, - to, - amount + + // We construct calldata for the `assetProxy.transferFrom` ABI. + // The layout of this calldata is in the table below. + // + // | Area | Offset | Length | Contents | + // | -------- |--------|---------|-------------------------------------------- | + // | Header | 0 | 4 | function selector | + // | Params | | 4 * 32 | function parameters: | + // | | 4 | | 1. offset to assetData (*) | + // | | 36 | | 2. from | + // | | 68 | | 3. to | + // | | 100 | | 4. amount | + // | Data | | | assetData: | + // | | 132 | 32 | assetData Length | + // | | 164 | ** | assetData Contents | + + bytes4 transferFromSelector = IAssetProxy(assetProxy).transferFrom.selector; + bool success; + assembly { + /////// Setup State /////// + // `cdStart` is the start of the calldata for `assetProxy.transferFrom` (equal to free memory ptr). + let cdStart := mload(64) + // `dataAreaLength` is the total number of words needed to store `assetData` + // As-per the ABI spec, this value is padded up to the nearest multiple of 32, + // and includes 32-bytes for length. + let dataAreaLength := and(add(mload(assetData), 63), 0xFFFFFFFFFFFE0) + // `cdEnd` is the end of the calldata for `assetProxy.transferFrom`. + let cdEnd := add(cdStart, add(132, dataAreaLength)) + + + /////// Setup Header Area /////// + // This area holds the 4-byte `transferFromSelector`. + mstore(cdStart, transferFromSelector) + + /////// Setup Params Area /////// + // Each parameter is padded to 32-bytes. The entire Params Area is 128 bytes. + // Notes: + // 1. The offset to `assetData` is the length of the Params Area (128 bytes). + // 2. A 20-byte mask is applied to addresses to zero-out the unused bytes. + mstore(add(cdStart, 4), 128) + mstore(add(cdStart, 36), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) + mstore(add(cdStart, 68), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) + mstore(add(cdStart, 100), amount) + + /////// Setup Data Area /////// + // This area holds `assetData`. + let dataArea := add(cdStart, 132) + for {} lt(dataArea, cdEnd) {} { + mstore(dataArea, mload(assetData)) + dataArea := add(dataArea, 32) + assetData := add(assetData, 32) + } + + /////// Call `assetProxy.transferFrom` using the constructed calldata /////// + success := call( + gas, // forward all gas + assetProxy, // call address of asset proxy + 0, // don't send any ETH + cdStart, // pointer to start of input + sub(cdEnd, cdStart), // length of input + cdStart, // write output over input + 0 // output size is 0 bytes + ) + } + require( + success, + "TRANSFER_FAILED" ); } } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol index b207b3e57..c0ed023ac 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol @@ -20,11 +20,9 @@ pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "./libs/LibConstants.sol"; -import "../../utils/LibBytes/LibBytes.sol"; import "./libs/LibFillResults.sol"; import "./libs/LibOrder.sol"; import "./libs/LibMath.sol"; -import "./libs/LibExchangeErrors.sol"; import "./mixins/MExchangeCore.sol"; import "./mixins/MSignatureValidator.sol"; import "./mixins/MTransactions.sol"; @@ -35,14 +33,11 @@ contract MixinExchangeCore is LibMath, LibOrder, LibFillResults, - LibExchangeErrors, MAssetProxyDispatcher, MExchangeCore, MSignatureValidator, MTransactions { - using LibBytes for bytes; - // Mapping of orderHash => amount of takerAsset already bought by maker mapping (bytes32 => uint256) public filled; @@ -73,7 +68,7 @@ contract MixinExchangeCore is // Ensure orderEpoch is monotonically increasing require( newOrderEpoch > oldOrderEpoch, - INVALID_NEW_ORDER_EPOCH + "INVALID_NEW_ORDER_EPOCH" ); // Update orderEpoch @@ -285,20 +280,20 @@ contract MixinExchangeCore is // An order can only be filled if its status is FILLABLE. require( orderInfo.orderStatus == uint8(OrderStatus.FILLABLE), - ORDER_UNFILLABLE + "ORDER_UNFILLABLE" ); // Revert if fill amount is invalid require( takerAssetFillAmount != 0, - INVALID_TAKER_AMOUNT + "INVALID_TAKER_AMOUNT" ); // Validate sender is allowed to fill this order if (order.senderAddress != address(0)) { require( order.senderAddress == msg.sender, - INVALID_SENDER + "INVALID_SENDER" ); } @@ -306,7 +301,7 @@ contract MixinExchangeCore is if (order.takerAddress != address(0)) { require( order.takerAddress == takerAddress, - INVALID_TAKER + "INVALID_TAKER" ); } @@ -318,7 +313,7 @@ contract MixinExchangeCore is order.makerAddress, signature ), - INVALID_ORDER_SIGNATURE + "INVALID_ORDER_SIGNATURE" ); } @@ -329,7 +324,7 @@ contract MixinExchangeCore is order.takerAssetAmount, order.makerAssetAmount ), - ROUNDING_ERROR + "ROUNDING_ERROR" ); } @@ -347,14 +342,14 @@ contract MixinExchangeCore is // An order can only be cancelled if its status is FILLABLE. require( orderInfo.orderStatus == uint8(OrderStatus.FILLABLE), - ORDER_UNFILLABLE + "ORDER_UNFILLABLE" ); // Validate sender is allowed to cancel this order if (order.senderAddress != address(0)) { require( order.senderAddress == msg.sender, - INVALID_SENDER + "INVALID_SENDER" ); } @@ -362,7 +357,7 @@ contract MixinExchangeCore is address makerAddress = getCurrentContextAddress(); require( order.makerAddress == makerAddress, - INVALID_MAKER + "INVALID_MAKER" ); } @@ -412,33 +407,27 @@ contract MixinExchangeCore is ) private { - uint8 makerAssetProxyId = uint8(order.makerAssetData.popLastByte()); - uint8 takerAssetProxyId = uint8(order.takerAssetData.popLastByte()); bytes memory zrxAssetData = ZRX_ASSET_DATA; dispatchTransferFrom( order.makerAssetData, - makerAssetProxyId, order.makerAddress, takerAddress, fillResults.makerAssetFilledAmount ); dispatchTransferFrom( order.takerAssetData, - takerAssetProxyId, takerAddress, order.makerAddress, fillResults.takerAssetFilledAmount ); dispatchTransferFrom( zrxAssetData, - ZRX_PROXY_ID, order.makerAddress, order.feeRecipientAddress, fillResults.makerFeePaid ); dispatchTransferFrom( zrxAssetData, - ZRX_PROXY_ID, takerAddress, order.feeRecipientAddress, fillResults.takerFeePaid diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinMatchOrders.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinMatchOrders.sol index e36fcc2c5..1a43eec79 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinMatchOrders.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinMatchOrders.sol @@ -15,11 +15,9 @@ pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "./libs/LibConstants.sol"; -import "../../utils/LibBytes/LibBytes.sol"; import "./libs/LibMath.sol"; import "./libs/LibOrder.sol"; import "./libs/LibFillResults.sol"; -import "./libs/LibExchangeErrors.sol"; import "./mixins/MExchangeCore.sol"; import "./mixins/MMatchOrders.sol"; import "./mixins/MTransactions.sol"; @@ -28,14 +26,11 @@ import "./mixins/MAssetProxyDispatcher.sol"; contract MixinMatchOrders is LibConstants, LibMath, - LibExchangeErrors, MAssetProxyDispatcher, MExchangeCore, MMatchOrders, MTransactions { - using LibBytes for bytes; - /// @dev Match two complementary orders that have a profitable spread. /// Each order is filled at their respective price point. However, the calculations are /// carried out as though the orders are both being filled at the right order's price point. @@ -144,7 +139,7 @@ contract MixinMatchOrders is require( safeMul(leftOrder.makerAssetAmount, rightOrder.makerAssetAmount) >= safeMul(leftOrder.takerAssetAmount, rightOrder.takerAssetAmount), - NEGATIVE_SPREAD_REQUIRED + "NEGATIVE_SPREAD_REQUIRED" ); } @@ -242,27 +237,22 @@ contract MixinMatchOrders is ) private { - uint8 leftMakerAssetProxyId = uint8(leftOrder.makerAssetData.popLastByte()); - uint8 rightMakerAssetProxyId = uint8(rightOrder.makerAssetData.popLastByte()); bytes memory zrxAssetData = ZRX_ASSET_DATA; // Order makers and taker dispatchTransferFrom( leftOrder.makerAssetData, - leftMakerAssetProxyId, leftOrder.makerAddress, rightOrder.makerAddress, matchedFillResults.right.takerAssetFilledAmount ); dispatchTransferFrom( rightOrder.makerAssetData, - rightMakerAssetProxyId, rightOrder.makerAddress, leftOrder.makerAddress, matchedFillResults.left.takerAssetFilledAmount ); dispatchTransferFrom( leftOrder.makerAssetData, - leftMakerAssetProxyId, leftOrder.makerAddress, takerAddress, matchedFillResults.leftMakerAssetSpreadAmount @@ -271,14 +261,12 @@ contract MixinMatchOrders is // Maker fees dispatchTransferFrom( zrxAssetData, - ZRX_PROXY_ID, leftOrder.makerAddress, leftOrder.feeRecipientAddress, matchedFillResults.left.makerFeePaid ); dispatchTransferFrom( zrxAssetData, - ZRX_PROXY_ID, rightOrder.makerAddress, rightOrder.feeRecipientAddress, matchedFillResults.right.makerFeePaid @@ -288,7 +276,6 @@ contract MixinMatchOrders is if (leftOrder.feeRecipientAddress == rightOrder.feeRecipientAddress) { dispatchTransferFrom( zrxAssetData, - ZRX_PROXY_ID, takerAddress, leftOrder.feeRecipientAddress, safeAdd( @@ -299,14 +286,12 @@ contract MixinMatchOrders is } else { dispatchTransferFrom( zrxAssetData, - ZRX_PROXY_ID, takerAddress, leftOrder.feeRecipientAddress, matchedFillResults.left.takerFeePaid ); dispatchTransferFrom( zrxAssetData, - ZRX_PROXY_ID, takerAddress, rightOrder.feeRecipientAddress, matchedFillResults.right.takerFeePaid diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinSignatureValidator.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinSignatureValidator.sol index cbb55bfce..29172057a 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinSignatureValidator.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinSignatureValidator.sol @@ -19,23 +19,17 @@ pragma solidity ^0.4.24; import "../../utils/LibBytes/LibBytes.sol"; -import "./libs/LibExchangeErrors.sol"; import "./mixins/MSignatureValidator.sol"; import "./mixins/MTransactions.sol"; import "./interfaces/IWallet.sol"; import "./interfaces/IValidator.sol"; contract MixinSignatureValidator is - LibExchangeErrors, MSignatureValidator, MTransactions { using LibBytes for bytes; - // Personal message headers - string constant ETH_PERSONAL_MESSAGE = "\x19Ethereum Signed Message:\n32"; - string constant TREZOR_PERSONAL_MESSAGE = "\x19Ethereum Signed Message:\n\x20"; - // Mapping of hash => signer => signed mapping (bytes32 => mapping (address => bool)) public preSigned; @@ -59,7 +53,7 @@ contract MixinSignatureValidator is signerAddress, signature ), - INVALID_SIGNATURE + "INVALID_SIGNATURE" ); preSigned[hash][signerAddress] = true; } @@ -99,14 +93,14 @@ contract MixinSignatureValidator is // TODO: Domain separation: make hash depend on role. (Taker sig should not be valid as maker sig, etc.) require( signature.length > 0, - LENGTH_GREATER_THAN_0_REQUIRED + "LENGTH_GREATER_THAN_0_REQUIRED" ); // Ensure signature is supported uint8 signatureTypeRaw = uint8(signature.popLastByte()); require( signatureTypeRaw < uint8(SignatureType.NSignatureTypes), - SIGNATURE_UNSUPPORTED + "SIGNATURE_UNSUPPORTED" ); // Pop last byte off of signature byte array. @@ -124,7 +118,7 @@ contract MixinSignatureValidator is // it an explicit option. This aids testing and analysis. It is // also the initialization value for the enum type. if (signatureType == SignatureType.Illegal) { - revert(SIGNATURE_ILLEGAL); + revert("SIGNATURE_ILLEGAL"); // Always invalid signature. // Like Illegal, this is always implicitly available and therefore @@ -133,7 +127,7 @@ contract MixinSignatureValidator is } else if (signatureType == SignatureType.Invalid) { require( signature.length == 0, - LENGTH_0_REQUIRED + "LENGTH_0_REQUIRED" ); isValid = false; return isValid; @@ -142,7 +136,7 @@ contract MixinSignatureValidator is } else if (signatureType == SignatureType.EIP712) { require( signature.length == 65, - LENGTH_65_REQUIRED + "LENGTH_65_REQUIRED" ); v = uint8(signature[0]); r = signature.readBytes32(1); @@ -155,13 +149,16 @@ contract MixinSignatureValidator is } else if (signatureType == SignatureType.EthSign) { require( signature.length == 65, - LENGTH_65_REQUIRED + "LENGTH_65_REQUIRED" ); v = uint8(signature[0]); r = signature.readBytes32(1); s = signature.readBytes32(33); recovered = ecrecover( - keccak256(abi.encodePacked(ETH_PERSONAL_MESSAGE, hash)), + keccak256(abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + hash + )), v, r, s @@ -180,7 +177,7 @@ contract MixinSignatureValidator is } else if (signatureType == SignatureType.Caller) { require( signature.length == 0, - LENGTH_0_REQUIRED + "LENGTH_0_REQUIRED" ); isValid = signerAddress == msg.sender; return isValid; @@ -230,13 +227,16 @@ contract MixinSignatureValidator is } else if (signatureType == SignatureType.Trezor) { require( signature.length == 65, - LENGTH_65_REQUIRED + "LENGTH_65_REQUIRED" ); v = uint8(signature[0]); r = signature.readBytes32(1); s = signature.readBytes32(33); recovered = ecrecover( - keccak256(abi.encodePacked(TREZOR_PERSONAL_MESSAGE, hash)), + keccak256(abi.encodePacked( + "\x19Ethereum Signed Message:\n\x20", + hash + )), v, r, s @@ -250,6 +250,6 @@ contract MixinSignatureValidator is // that we currently support. In this case returning false // may lead the caller to incorrectly believe that the // signature was invalid.) - revert(SIGNATURE_UNSUPPORTED); + revert("SIGNATURE_UNSUPPORTED"); } } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol index 20a4a12df..e0f450d0a 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol @@ -20,12 +20,10 @@ pragma solidity ^0.4.24; import "./libs/LibExchangeErrors.sol"; import "./mixins/MSignatureValidator.sol"; import "./mixins/MTransactions.sol"; -import "./libs/LibExchangeErrors.sol"; import "./libs/LibEIP712.sol"; contract MixinTransactions is LibEIP712, - LibExchangeErrors, MSignatureValidator, MTransactions { @@ -90,7 +88,7 @@ contract MixinTransactions is // Prevent reentrancy require( currentContextAddress == address(0), - REENTRANCY_ILLEGAL + "REENTRANCY_ILLEGAL" ); bytes32 transactionHash = hashEIP712Message(hashZeroExTransaction( @@ -102,7 +100,7 @@ contract MixinTransactions is // Validate transaction has not been executed require( !transactions[transactionHash], - INVALID_TX_HASH + "INVALID_TX_HASH" ); // Transaction always valid if signer is sender of transaction @@ -114,7 +112,7 @@ contract MixinTransactions is signerAddress, signature ), - INVALID_TX_SIGNATURE + "INVALID_TX_SIGNATURE" ); // Set the current transaction signer @@ -125,7 +123,7 @@ contract MixinTransactions is transactions[transactionHash] = true; require( address(this).delegatecall(data), - FAILED_EXECUTION + "FAILED_EXECUTION" ); // Reset current transaction signer diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol index 724f95518..00668ca43 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol @@ -22,13 +22,11 @@ pragma experimental ABIEncoderV2; import "./libs/LibMath.sol"; import "./libs/LibOrder.sol"; import "./libs/LibFillResults.sol"; -import "./libs/LibExchangeErrors.sol"; import "./mixins/MExchangeCore.sol"; contract MixinWrapperFunctions is LibMath, LibFillResults, - LibExchangeErrors, MExchangeCore { /// @dev Fills the input order. Reverts if exact takerAssetFillAmount not filled. @@ -50,7 +48,7 @@ contract MixinWrapperFunctions is ); require( fillResults.takerAssetFilledAmount == takerAssetFillAmount, - COMPLETE_FILL_FAILED + "COMPLETE_FILL_FAILED" ); return fillResults; } @@ -366,14 +364,6 @@ contract MixinWrapperFunctions is signatures[i] ); - // HACK: the proxyId is "popped" from the byte array before a fill is settled - // by subtracting from the length of the array. Since the popped byte is - // still in memory, we can "unpop" it by incrementing the length of the byte array. - assembly { - let len := mload(takerAssetData) - mstore(takerAssetData, add(len, 1)) - } - // Update amounts filled and fees paid by maker and taker addFillResults(totalFillResults, singleFillResults); @@ -467,14 +457,6 @@ contract MixinWrapperFunctions is signatures[i] ); - // HACK: the proxyId is "popped" from the byte array before a fill is settled - // by subtracting from the length of the array. Since the popped byte is - // still in memory, we can "unpop" it by incrementing the length of the byte array. - assembly { - let len := mload(makerAssetData) - mstore(makerAssetData, add(len, 1)) - } - // Update amounts filled and fees paid by maker and taker addFillResults(totalFillResults, singleFillResults); diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol index 2c331dc34..fa55dff00 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol @@ -26,7 +26,7 @@ contract IAssetProxyDispatcher { /// @param newAssetProxy Address of new asset proxy to register, or 0x0 to unset assetProxyId. /// @param oldAssetProxy Existing asset proxy to overwrite, or 0x0 if assetProxyId is currently unused. function registerAssetProxy( - uint8 assetProxyId, + bytes4 assetProxyId, address newAssetProxy, address oldAssetProxy ) @@ -35,7 +35,7 @@ contract IAssetProxyDispatcher { /// @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) + function getAssetProxy(bytes4 assetProxyId) external view returns (address); diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibConstants.sol b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibConstants.sol index 4a9452448..488ca956c 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibConstants.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibConstants.sol @@ -25,9 +25,6 @@ contract LibConstants { // not constant to make testing easier. bytes public ZRX_ASSET_DATA; - // Proxy Id for ZRX token. - uint8 constant ZRX_PROXY_ID = 1; - // @TODO: Remove when we deploy. constructor (bytes memory zrxAssetData) public diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibExchangeErrors.sol b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibExchangeErrors.sol index a43f0f927..e37f41ada 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibExchangeErrors.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibExchangeErrors.sol @@ -18,7 +18,10 @@ pragma solidity ^0.4.24; +/// @dev This contract documents the revert reasons used in the Exchange contract. +/// This contract is intended to serve as a reference, but is not actually used for efficiency reasons. contract LibExchangeErrors { + /// Order validation errors /// string constant ORDER_UNFILLABLE = "ORDER_UNFILLABLE"; // Order cannot be filled. string constant INVALID_MAKER = "INVALID_MAKER"; // Invalid makerAddress. @@ -56,9 +59,11 @@ contract LibExchangeErrors { /// dispatchTransferFrom errors /// string constant ASSET_PROXY_DOES_NOT_EXIST = "ASSET_PROXY_DOES_NOT_EXIST"; // No assetProxy registered at given id. + string constant TRANSFER_FAILED = "TRANSFER_FAILED"; // Asset transfer unsuccesful. /// Length validation errors /// string constant LENGTH_GREATER_THAN_0_REQUIRED = "LENGTH_GREATER_THAN_0_REQUIRED"; // Byte array must have a length greater than 0. + string constant LENGTH_GREATER_THAN_3_REQUIRED = "LENGTH_GREATER_THAN_3_REQUIRED"; // Byte array must have a length greater than 3. string constant LENGTH_0_REQUIRED = "LENGTH_0_REQUIRED"; // Byte array must have a length of 0. string constant LENGTH_65_REQUIRED = "LENGTH_65_REQUIRED"; // Byte array must have a length of 65. } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibMath.sol b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibMath.sol index 233547b9f..bfe2fd33f 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibMath.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibMath.sol @@ -23,7 +23,6 @@ import "../../../utils/SafeMath/SafeMath.sol"; contract LibMath is SafeMath { - string constant ROUNDING_ERROR_ON_PARTIAL_AMOUNT = "A rounding error occurred when calculating partial transfer amounts."; /// @dev Calculates partial value given a numerator and denominator. /// @param numerator Numerator. diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MAssetProxyDispatcher.sol b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MAssetProxyDispatcher.sol index 788f42c60..c2b506dcf 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MAssetProxyDispatcher.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MAssetProxyDispatcher.sol @@ -27,20 +27,18 @@ contract MAssetProxyDispatcher is // Logs registration of new asset proxy event AssetProxySet( - uint8 id, // Id of new registered AssetProxy. + bytes4 id, // Id of new registered AssetProxy. address newAssetProxy, // Address of new registered AssetProxy. address oldAssetProxy // Address of AssetProxy that was overwritten at given id (or null address). ); /// @dev Forwards arguments to assetProxy and calls `transferFrom`. Either succeeds or throws. - /// @param assetData Byte array encoded for the respective asset proxy. - /// @param assetProxyId Id of assetProxy to dispach to. + /// @param assetData Byte array encoded for the asset. /// @param from Address to transfer token from. /// @param to Address to transfer token to. /// @param amount Amount of token to transfer. function dispatchTransferFrom( bytes memory assetData, - uint8 assetProxyId, address from, address to, uint256 amount diff --git a/packages/contracts/src/contracts/current/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol b/packages/contracts/src/contracts/current/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol index d469a07f0..2ae69e0ef 100644 --- a/packages/contracts/src/contracts/current/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol +++ b/packages/contracts/src/contracts/current/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol @@ -24,12 +24,11 @@ import "../../protocol/Exchange/MixinAssetProxyDispatcher.sol"; contract TestAssetProxyDispatcher is MixinAssetProxyDispatcher { function publicDispatchTransferFrom( bytes memory assetData, - uint8 assetProxyId, address from, address to, uint256 amount) public { - dispatchTransferFrom(assetData, assetProxyId, from, to, amount); + dispatchTransferFrom(assetData, from, to, amount); } } diff --git a/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol b/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol index e4cbf318b..78b1ddf7c 100644 --- a/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol +++ b/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol @@ -19,17 +19,8 @@ pragma solidity ^0.4.24; library LibBytes { - using LibBytes for bytes; - // Revert reasons - string constant GREATER_THAN_ZERO_LENGTH_REQUIRED = "GREATER_THAN_ZERO_LENGTH_REQUIRED"; - string constant GREATER_OR_EQUAL_TO_4_LENGTH_REQUIRED = "GREATER_OR_EQUAL_TO_4_LENGTH_REQUIRED"; - string constant GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED = "GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED"; - string constant GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED = "GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED"; - string constant GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED = "GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED"; - string constant GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED = "GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED"; - string constant FROM_LESS_THAN_TO_REQUIRED = "FROM_LESS_THAN_TO_REQUIRED"; - string constant TO_LESS_THAN_LENGTH_REQUIRED = "TO_LESS_THAN_LENGTH_REQUIRED"; + using LibBytes for bytes; /// @dev Gets the memory address for a byte array. /// @param input Byte array to lookup. @@ -176,8 +167,14 @@ library LibBytes { pure returns (bytes memory result) { - require(from <= to, FROM_LESS_THAN_TO_REQUIRED); - require(to < b.length, TO_LESS_THAN_LENGTH_REQUIRED); + require( + from <= to, + "FROM_LESS_THAN_TO_REQUIRED" + ); + require( + to < b.length, + "TO_LESS_THAN_LENGTH_REQUIRED" + ); // Create a new bytes structure and copy contents result = new bytes(to - from); @@ -199,8 +196,14 @@ library LibBytes { pure returns (bytes memory result) { - require(from <= to, FROM_LESS_THAN_TO_REQUIRED); - require(to < b.length, TO_LESS_THAN_LENGTH_REQUIRED); + require( + from <= to, + "FROM_LESS_THAN_TO_REQUIRED" + ); + require( + to < b.length, + "TO_LESS_THAN_LENGTH_REQUIRED" + ); // Create a new bytes structure around [from, to) in-place. assembly { @@ -220,7 +223,7 @@ library LibBytes { { require( b.length > 0, - GREATER_THAN_ZERO_LENGTH_REQUIRED + "GREATER_THAN_ZERO_LENGTH_REQUIRED" ); // Store last byte. @@ -244,7 +247,7 @@ library LibBytes { { require( b.length >= 20, - GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED + "GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED" ); // Store last 20 bytes. @@ -290,7 +293,7 @@ library LibBytes { { require( b.length >= index + 20, // 20 is length of address - GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED + "GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED" ); // Add offset to index: @@ -322,7 +325,7 @@ library LibBytes { { require( b.length >= index + 20, // 20 is length of address - GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED + "GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED" ); // Add offset to index: @@ -365,7 +368,7 @@ library LibBytes { { require( b.length >= index + 32, - GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED + "GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED" ); // Arrays are prefixed by a 256 bit length parameter @@ -392,7 +395,7 @@ library LibBytes { { require( b.length >= index + 32, - GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED + "GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED" ); // Arrays are prefixed by a 256 bit length parameter @@ -447,7 +450,7 @@ library LibBytes { { require( b.length >= index + 4, - GREATER_OR_EQUAL_TO_4_LENGTH_REQUIRED + "GREATER_OR_EQUAL_TO_4_LENGTH_REQUIRED" ); assembly { result := mload(add(b, 32)) @@ -459,6 +462,8 @@ library LibBytes { } /// @dev Reads nested bytes from a specific position. + /// @dev NOTE: the returned value overlaps with the input value. + /// Both should be treated as immutable. /// @param b Byte array containing nested bytes. /// @param index Index of nested bytes. /// @return result Nested bytes. @@ -478,17 +483,13 @@ library LibBytes { // length of nested bytes require( b.length >= index + nestedBytesLength, - GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED + "GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED" ); - - // Allocate memory and copy value to result - result = new bytes(nestedBytesLength); - memCopy( - result.contentAddress(), - b.contentAddress() + index, - nestedBytesLength - ); - + + // Return a pointer to the byte array as it exists inside `b` + assembly { + result := add(b, index) + } return result; } @@ -508,7 +509,7 @@ library LibBytes { // length of input require( b.length >= index + 32 /* 32 bytes to store length */ + input.length, - GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED + "GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED" ); // Copy <input> into <b> @@ -533,7 +534,7 @@ library LibBytes { // Dest length must be >= source length, or some bytes would not be copied. require( dest.length >= sourceLen, - GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED + "GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED" ); memCopy( dest.contentAddress(), diff --git a/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol b/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol index 99b93d0d2..6f5761cc7 100644 --- a/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol +++ b/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol @@ -13,9 +13,6 @@ import "./IOwnable.sol"; contract Ownable is IOwnable { address public owner; - // Revert reasons - string constant ONLY_CONTRACT_OWNER = "ONLY_CONTRACT_OWNER"; - constructor () public { @@ -25,7 +22,7 @@ contract Ownable is IOwnable { modifier onlyOwner() { require( msg.sender == owner, - ONLY_CONTRACT_OWNER + "ONLY_CONTRACT_OWNER" ); _; } diff --git a/packages/contracts/test/asset_proxy/decoder.ts b/packages/contracts/test/asset_proxy/decoder.ts index 98d18aa38..902255caf 100644 --- a/packages/contracts/test/asset_proxy/decoder.ts +++ b/packages/contracts/test/asset_proxy/decoder.ts @@ -44,7 +44,6 @@ describe('TestAssetDataDecoders', () => { it('should correctly decode ERC721 asset data', async () => { const tokenId = generatePseudoRandomSalt(); const encodedAssetData = assetProxyUtils.encodeERC721AssetData(testAddress, tokenId); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); const expectedDecodedAssetData = assetProxyUtils.decodeERC721AssetData(encodedAssetData); let decodedTokenAddress: string; let decodedTokenId: BigNumber; @@ -53,7 +52,7 @@ describe('TestAssetDataDecoders', () => { decodedTokenAddress, decodedTokenId, decodedData, - ] = await testAssetProxyDecoder.publicDecodeERC721Data.callAsync(encodedAssetDataWithoutProxyId); + ] = await testAssetProxyDecoder.publicDecodeERC721Data.callAsync(encodedAssetData); expect(decodedTokenAddress).to.be.equal(expectedDecodedAssetData.tokenAddress); expect(decodedTokenId).to.be.bignumber.equal(expectedDecodedAssetData.tokenId); expect(decodedData).to.be.equal(expectedDecodedAssetData.receiverData); diff --git a/packages/contracts/test/asset_proxy/proxies.ts b/packages/contracts/test/asset_proxy/proxies.ts index d07d82235..0457eb3ef 100644 --- a/packages/contracts/test/asset_proxy/proxies.ts +++ b/packages/contracts/test/asset_proxy/proxies.ts @@ -100,13 +100,12 @@ describe('Asset Transfer Proxies', () => { it('should successfully transfer tokens', async () => { // Construct ERC20 asset data const encodedAssetData = assetProxyUtils.encodeERC20AssetData(zrxToken.address); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); // Perform a transfer from makerAddress to takerAddress const erc20Balances = await erc20Wrapper.getBalancesAsync(); const amount = new BigNumber(10); await web3Wrapper.awaitTransactionSuccessAsync( await erc20Proxy.transferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, + encodedAssetData, makerAddress, takerAddress, amount, @@ -127,13 +126,12 @@ describe('Asset Transfer Proxies', () => { it('should do nothing if transferring 0 amount of a token', async () => { // Construct ERC20 asset data const encodedAssetData = assetProxyUtils.encodeERC20AssetData(zrxToken.address); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); // Perform a transfer from makerAddress to takerAddress const erc20Balances = await erc20Wrapper.getBalancesAsync(); const amount = new BigNumber(0); await web3Wrapper.awaitTransactionSuccessAsync( await erc20Proxy.transferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, + encodedAssetData, makerAddress, takerAddress, amount, @@ -179,19 +177,13 @@ describe('Asset Transfer Proxies', () => { it('should throw if requesting address is not authorized', async () => { // Construct ERC20 asset data const encodedAssetData = assetProxyUtils.encodeERC20AssetData(zrxToken.address); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); + // Perform a transfer from makerAddress to takerAddress const amount = new BigNumber(10); return expectRevertReasonOrAlwaysFailingTransactionAsync( - erc20Proxy.transferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, - makerAddress, - takerAddress, - amount, - { - from: notAuthorized, - }, - ), + erc20Proxy.transferFrom.sendTransactionAsync(encodedAssetData, makerAddress, takerAddress, amount, { + from: notAuthorized, + }), RevertReasons.SenderNotAuthorized, ); }); @@ -202,10 +194,9 @@ describe('Asset Transfer Proxies', () => { const erc20Balances = await erc20Wrapper.getBalancesAsync(); const encodedAssetData = assetProxyUtils.encodeERC20AssetData(zrxToken.address); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); const amount = new BigNumber(10); const numTransfers = 2; - const assetData = _.times(numTransfers, () => encodedAssetDataWithoutProxyId); + const assetData = _.times(numTransfers, () => encodedAssetData); const fromAddresses = _.times(numTransfers, () => makerAddress); const toAddresses = _.times(numTransfers, () => takerAddress); const amounts = _.times(numTransfers, () => amount); @@ -234,10 +225,9 @@ describe('Asset Transfer Proxies', () => { it('should throw if not called by an authorized address', async () => { const encodedAssetData = assetProxyUtils.encodeERC20AssetData(zrxToken.address); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); const amount = new BigNumber(10); const numTransfers = 2; - const assetData = _.times(numTransfers, () => encodedAssetDataWithoutProxyId); + const assetData = _.times(numTransfers, () => encodedAssetData); const fromAddresses = _.times(numTransfers, () => makerAddress); const toAddresses = _.times(numTransfers, () => takerAddress); const amounts = _.times(numTransfers, () => amount); @@ -251,9 +241,10 @@ describe('Asset Transfer Proxies', () => { }); }); - it('should have an id of 1', async () => { + it('should have an id of 0xf47261b0', async () => { const proxyId = await erc20Proxy.getProxyId.callAsync(); - expect(proxyId).to.equal(1); + const expectedProxyId = '0xf47261b0'; + expect(proxyId).to.equal(expectedProxyId); }); }); @@ -262,7 +253,6 @@ describe('Asset Transfer Proxies', () => { it('should successfully transfer tokens', async () => { // Construct ERC721 asset data const encodedAssetData = assetProxyUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); // Verify pre-condition const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); @@ -270,7 +260,7 @@ describe('Asset Transfer Proxies', () => { const amount = new BigNumber(1); await web3Wrapper.awaitTransactionSuccessAsync( await erc721Proxy.transferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, + encodedAssetData, makerAddress, takerAddress, amount, @@ -286,14 +276,13 @@ describe('Asset Transfer Proxies', () => { it('should call onERC721Received when transferring to a smart contract without receiver data', async () => { // Construct ERC721 asset data const encodedAssetData = assetProxyUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); // Verify pre-condition const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); // Perform a transfer from makerAddress to takerAddress const amount = new BigNumber(1); const txHash = await erc721Proxy.transferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, + encodedAssetData, makerAddress, erc721Receiver.address, amount, @@ -322,14 +311,13 @@ describe('Asset Transfer Proxies', () => { erc721MakerTokenId, receiverData, ); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); // Verify pre-condition const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); // Perform a transfer from makerAddress to takerAddress const amount = new BigNumber(1); const txHash = await erc721Proxy.transferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, + encodedAssetData, makerAddress, erc721Receiver.address, amount, @@ -358,7 +346,6 @@ describe('Asset Transfer Proxies', () => { erc721MakerTokenId, receiverData, ); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); // Verify pre-condition const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); @@ -366,7 +353,7 @@ describe('Asset Transfer Proxies', () => { const amount = new BigNumber(1); return expectRevertOrAlwaysFailingTransactionAsync( erc721Proxy.transferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, + encodedAssetData, makerAddress, erc20Proxy.address, // the ERC20 proxy does not have an ERC721 receiver amount, @@ -378,7 +365,6 @@ describe('Asset Transfer Proxies', () => { it('should throw if transferring 0 amount of a token', async () => { // Construct ERC721 asset data const encodedAssetData = assetProxyUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); // Verify pre-condition const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); @@ -386,7 +372,7 @@ describe('Asset Transfer Proxies', () => { const amount = new BigNumber(0); return expectRevertReasonOrAlwaysFailingTransactionAsync( erc721Proxy.transferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, + encodedAssetData, makerAddress, takerAddress, amount, @@ -399,7 +385,6 @@ describe('Asset Transfer Proxies', () => { it('should throw if transferring > 1 amount of a token', async () => { // Construct ERC721 asset data const encodedAssetData = assetProxyUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); // Verify pre-condition const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); expect(ownerMakerAsset).to.be.bignumber.equal(makerAddress); @@ -407,7 +392,7 @@ describe('Asset Transfer Proxies', () => { const amount = new BigNumber(500); return expectRevertReasonOrAlwaysFailingTransactionAsync( erc721Proxy.transferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, + encodedAssetData, makerAddress, takerAddress, amount, @@ -420,7 +405,6 @@ describe('Asset Transfer Proxies', () => { it('should throw if allowances are too low', async () => { // Construct ERC721 asset data const encodedAssetData = assetProxyUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); // Remove transfer approval for makerAddress. await web3Wrapper.awaitTransactionSuccessAsync( await erc721Token.setApprovalForAll.sendTransactionAsync(erc721Proxy.address, false, { @@ -431,15 +415,9 @@ describe('Asset Transfer Proxies', () => { // Perform a transfer; expect this to fail. const amount = new BigNumber(1); return expectRevertReasonOrAlwaysFailingTransactionAsync( - erc20Proxy.transferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, - makerAddress, - takerAddress, - amount, - { - from: exchangeAddress, - }, - ), + erc20Proxy.transferFrom.sendTransactionAsync(encodedAssetData, makerAddress, takerAddress, amount, { + from: exchangeAddress, + }), RevertReasons.TransferFailed, ); }); @@ -447,12 +425,11 @@ describe('Asset Transfer Proxies', () => { it('should throw if requesting address is not authorized', async () => { // Construct ERC721 asset data const encodedAssetData = assetProxyUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); // Perform a transfer from makerAddress to takerAddress const amount = new BigNumber(1); return expectRevertReasonOrAlwaysFailingTransactionAsync( erc721Proxy.transferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, + encodedAssetData, makerAddress, takerAddress, amount, @@ -470,8 +447,8 @@ describe('Asset Transfer Proxies', () => { const numTransfers = 2; const assetData = [ - assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerTokenIdA).slice(0, -2), - assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerTokenIdB).slice(0, -2), + assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerTokenIdA), + assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerTokenIdB), ]; const fromAddresses = _.times(numTransfers, () => makerAddress); const toAddresses = _.times(numTransfers, () => takerAddress); @@ -518,9 +495,10 @@ describe('Asset Transfer Proxies', () => { }); }); - it('should have an id of 2', async () => { + it('should have an id of 0x08e937fa', async () => { const proxyId = await erc721Proxy.getProxyId.callAsync(); - expect(proxyId).to.equal(2); + const expectedProxyId = '0x08e937fa'; + expect(proxyId).to.equal(expectedProxyId); }); }); }); diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index 3b8a369b4..c83041638 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/packages/contracts/test/exchange/core.ts @@ -89,7 +89,7 @@ describe('Exchange core', () => { artifacts.Exchange, provider, txDefaults, - zrxToken.address, + assetProxyUtils.encodeERC20AssetData(zrxToken.address), ); exchangeWrapper = new ExchangeWrapper(exchange, provider); await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC20, erc20Proxy.address, owner); diff --git a/packages/contracts/test/exchange/dispatcher.ts b/packages/contracts/test/exchange/dispatcher.ts index cf73d274b..495f6d3c8 100644 --- a/packages/contracts/test/exchange/dispatcher.ts +++ b/packages/contracts/test/exchange/dispatcher.ts @@ -279,14 +279,13 @@ describe('AssetProxyDispatcher', () => { ); // Construct metadata for ERC20 proxy const encodedAssetData = assetProxyUtils.encodeERC20AssetData(zrxToken.address); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); + // Perform a transfer from makerAddress to takerAddress const erc20Balances = await erc20Wrapper.getBalancesAsync(); const amount = new BigNumber(10); await web3Wrapper.awaitTransactionSuccessAsync( await assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, - AssetProxyId.ERC20, + encodedAssetData, makerAddress, takerAddress, amount, @@ -307,13 +306,11 @@ describe('AssetProxyDispatcher', () => { it('should throw if dispatching to unregistered proxy', async () => { // Construct metadata for ERC20 proxy const encodedAssetData = assetProxyUtils.encodeERC20AssetData(zrxToken.address); - const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2); // Perform a transfer from makerAddress to takerAddress const amount = new BigNumber(10); return expectRevertReasonOrAlwaysFailingTransactionAsync( assetProxyDispatcher.publicDispatchTransferFrom.sendTransactionAsync( - encodedAssetDataWithoutProxyId, - AssetProxyId.ERC20, + encodedAssetData, makerAddress, takerAddress, amount, diff --git a/packages/contracts/test/exchange/match_orders.ts b/packages/contracts/test/exchange/match_orders.ts index 324cf319f..6f4feb8b7 100644 --- a/packages/contracts/test/exchange/match_orders.ts +++ b/packages/contracts/test/exchange/match_orders.ts @@ -96,7 +96,7 @@ describe('matchOrders', () => { artifacts.Exchange, provider, txDefaults, - zrxToken.address, + assetProxyUtils.encodeERC20AssetData(zrxToken.address), ); exchangeWrapper = new ExchangeWrapper(exchange, provider); await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC20, erc20Proxy.address, owner); diff --git a/packages/contracts/test/exchange/transactions.ts b/packages/contracts/test/exchange/transactions.ts index 8d4799109..44ed98c53 100644 --- a/packages/contracts/test/exchange/transactions.ts +++ b/packages/contracts/test/exchange/transactions.ts @@ -79,7 +79,7 @@ describe('Exchange transactions', () => { artifacts.Exchange, provider, txDefaults, - zrxToken.address, + assetProxyUtils.encodeERC20AssetData(zrxToken.address), ); exchangeWrapper = new ExchangeWrapper(exchange, provider); await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC20, erc20Proxy.address, owner); diff --git a/packages/contracts/test/exchange/wrapper.ts b/packages/contracts/test/exchange/wrapper.ts index 3ea197395..a4ba54904 100644 --- a/packages/contracts/test/exchange/wrapper.ts +++ b/packages/contracts/test/exchange/wrapper.ts @@ -80,7 +80,7 @@ describe('Exchange wrappers', () => { artifacts.Exchange, provider, txDefaults, - zrxToken.address, + assetProxyUtils.encodeERC20AssetData(zrxToken.address), ); exchangeWrapper = new ExchangeWrapper(exchange, provider); await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC20, erc20Proxy.address, owner); diff --git a/packages/order-utils/src/asset_proxy_utils.ts b/packages/order-utils/src/asset_proxy_utils.ts index 915ee5032..8140ad89d 100644 --- a/packages/order-utils/src/asset_proxy_utils.ts +++ b/packages/order-utils/src/asset_proxy_utils.ts @@ -1,32 +1,43 @@ import { AssetProxyId, ERC20AssetData, ERC721AssetData } from '@0xproject/types'; -import { BigNumber, NULL_BYTES } from '@0xproject/utils'; +import { BigNumber } from '@0xproject/utils'; import BN = require('bn.js'); import ethUtil = require('ethereumjs-util'); -import * as _ from 'lodash'; -const ERC20_ASSET_DATA_BYTE_LENGTH = 21; -const ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH = 53; -const ASSET_DATA_ADDRESS_OFFSET = 0; -const ERC721_ASSET_DATA_TOKEN_ID_OFFSET = 20; -const ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET = 52; -const ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET = 84; +import { constants } from './constants'; + +// TODO: Push upstream to DefinitelyTyped +interface EthAbi { + simpleEncode(signature: string, ...args: any[]): Buffer; + rawDecode(signature: string[], data: Buffer): any[]; +} +// tslint:disable:no-var-requires +const ethAbi = require('ethereumjs-abi') as EthAbi; export const assetProxyUtils = { encodeAssetProxyId(assetProxyId: AssetProxyId): Buffer { return ethUtil.toBuffer(assetProxyId); }, decodeAssetProxyId(encodedAssetProxyId: Buffer): AssetProxyId { - return ethUtil.bufferToInt(encodedAssetProxyId); + const hexString = ethUtil.bufferToHex(encodedAssetProxyId); + if (hexString === AssetProxyId.ERC20) { + return AssetProxyId.ERC20; + } + if (hexString === AssetProxyId.ERC721) { + return AssetProxyId.ERC721; + } + throw new Error(`Invalid ProxyId: ${hexString}`); }, encodeAddress(address: string): Buffer { if (!ethUtil.isValidAddress(address)) { throw new Error(`Invalid Address: ${address}`); } const encodedAddress = ethUtil.toBuffer(address); - return encodedAddress; + const padded = ethUtil.setLengthLeft(encodedAddress, constants.WORD_LENGTH); + return padded; }, decodeAddress(encodedAddress: Buffer): string { - const address = ethUtil.bufferToHex(encodedAddress); + const unpadded = ethUtil.setLengthLeft(encodedAddress, constants.ADDRESS_LENGTH); + const address = ethUtil.bufferToHex(unpadded); if (!ethUtil.isValidAddress(address)) { throw new Error(`Invalid Address: ${address}`); } @@ -37,33 +48,27 @@ export const assetProxyUtils = { const formattedValue = new BN(value.toString(base)); const encodedValue = ethUtil.toBuffer(formattedValue); // tslint:disable-next-line:custom-no-magic-numbers - const paddedValue = ethUtil.setLengthLeft(encodedValue, 32); + const paddedValue = ethUtil.setLengthLeft(encodedValue, constants.WORD_LENGTH); return paddedValue; }, decodeUint256(encodedValue: Buffer): BigNumber { const formattedValue = ethUtil.bufferToHex(encodedValue); - const value = new BigNumber(formattedValue, 16); + const value = new BigNumber(formattedValue, constants.BASE_16); return value; }, encodeERC20AssetData(tokenAddress: string): string { - const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC20); - const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); - const encodedAssetData = Buffer.concat([encodedAddress, encodedAssetProxyId]); - const encodedAssetDataHex = ethUtil.bufferToHex(encodedAssetData); - return encodedAssetDataHex; + return ethUtil.bufferToHex(ethAbi.simpleEncode('ERC20Token(address)', tokenAddress)); }, - decodeERC20AssetData(proxyData: string): ERC20AssetData { - const encodedAssetData = ethUtil.toBuffer(proxyData); - if (encodedAssetData.byteLength !== ERC20_ASSET_DATA_BYTE_LENGTH) { + decodeERC20AssetData(assetData: string): ERC20AssetData { + const data = ethUtil.toBuffer(assetData); + if (data.byteLength < constants.ERC20_ASSET_DATA_BYTE_LENGTH) { throw new Error( - `Could not decode ERC20 Proxy Data. Expected length of encoded data to be 21. Got ${ - encodedAssetData.byteLength - }`, + `Could not decode ERC20 Proxy Data. Expected length of encoded data to be at least ${ + constants.ERC20_ASSET_DATA_BYTE_LENGTH + }. Got ${data.byteLength}`, ); } - const assetProxyIdOffset = encodedAssetData.byteLength - 1; - const encodedAssetProxyId = encodedAssetData.slice(assetProxyIdOffset); - const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); + const assetProxyId = ethUtil.bufferToHex(data.slice(0, constants.SELECTOR_LENGTH)); if (assetProxyId !== AssetProxyId.ERC20) { throw new Error( `Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be ERC20 (${ @@ -71,98 +76,61 @@ export const assetProxyUtils = { }), but got ${assetProxyId}`, ); } - const encodedTokenAddress = encodedAssetData.slice(ASSET_DATA_ADDRESS_OFFSET, assetProxyIdOffset); - const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); - const erc20AssetData = { + const [tokenAddress] = ethAbi.rawDecode(['address'], data.slice(constants.SELECTOR_LENGTH)); + return { assetProxyId, - tokenAddress, + tokenAddress: ethUtil.addHexPrefix(tokenAddress), }; - return erc20AssetData; }, encodeERC721AssetData(tokenAddress: string, tokenId: BigNumber, receiverData?: string): string { - const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC721); - const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); - const encodedTokenId = assetProxyUtils.encodeUint256(tokenId); - let encodedAssetData = Buffer.concat([encodedAddress, encodedTokenId]); - if (!_.isUndefined(receiverData)) { - const encodedReceiverData = ethUtil.toBuffer(receiverData); - const receiverDataLength = new BigNumber(encodedReceiverData.byteLength); - const encodedReceiverDataLength = assetProxyUtils.encodeUint256(receiverDataLength); - encodedAssetData = Buffer.concat([encodedAssetData, encodedReceiverDataLength, encodedReceiverData]); - } - encodedAssetData = Buffer.concat([encodedAssetData, encodedAssetProxyId]); - const encodedAssetDataHex = ethUtil.bufferToHex(encodedAssetData); - return encodedAssetDataHex; + // TODO: Pass `tokendId` as a BigNumber. + return ethUtil.bufferToHex( + ethAbi.simpleEncode( + 'ERC721Token(address,uint256,bytes)', + tokenAddress, + `0x${tokenId.toString(constants.BASE_16)}`, + ethUtil.toBuffer(receiverData || '0x'), + ), + ); }, decodeERC721AssetData(assetData: string): ERC721AssetData { - const encodedAssetData = ethUtil.toBuffer(assetData); - if (encodedAssetData.byteLength < ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH) { + const data = ethUtil.toBuffer(assetData); + if (data.byteLength < constants.ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH) { throw new Error( - `Could not decode ERC20 Proxy Data. Expected length of encoded data to be at least 53. Got ${ - encodedAssetData.byteLength - }`, + `Could not decode ERC721 Asset Data. Expected length of encoded data to be at least ${ + constants.ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH + }. Got ${data.byteLength}`, ); } - - const encodedTokenAddress = encodedAssetData.slice( - ASSET_DATA_ADDRESS_OFFSET, - ERC721_ASSET_DATA_TOKEN_ID_OFFSET, - ); - const proxyIdOffset = encodedAssetData.byteLength - 1; - const encodedAssetProxyId = encodedAssetData.slice(proxyIdOffset); - const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); + const assetProxyId = ethUtil.bufferToHex(data.slice(0, constants.SELECTOR_LENGTH)); if (assetProxyId !== AssetProxyId.ERC721) { throw new Error( - `Could not decode ERC721 Proxy Data. Expected Asset Proxy Id to be ERC721 (${ + `Could not decode ERC721 Asset Data. Expected Asset Proxy Id to be ERC721 (${ AssetProxyId.ERC721 }), but got ${assetProxyId}`, ); } - const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); - const encodedTokenId = encodedAssetData.slice( - ERC721_ASSET_DATA_TOKEN_ID_OFFSET, - ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET, + const [tokenAddress, tokenId, receiverData] = ethAbi.rawDecode( + ['address', 'uint256', 'bytes'], + data.slice(constants.SELECTOR_LENGTH), ); - const tokenId = assetProxyUtils.decodeUint256(encodedTokenId); - let receiverData = NULL_BYTES; - const lengthUpToReceiverDataLength = ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET + 1; - if (encodedAssetData.byteLength > lengthUpToReceiverDataLength) { - const encodedReceiverDataLength = encodedAssetData.slice( - ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET, - ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET, - ); - const receiverDataLength = assetProxyUtils.decodeUint256(encodedReceiverDataLength); - const lengthUpToReceiverData = ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET + 1; - const expectedReceiverDataLength = new BigNumber(encodedAssetData.byteLength - lengthUpToReceiverData); - if (!receiverDataLength.equals(expectedReceiverDataLength)) { - throw new Error( - `Data length (${receiverDataLength}) does not match actual length of data (${expectedReceiverDataLength})`, - ); - } - const encodedReceiverData = encodedAssetData.slice( - ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET, - receiverDataLength.add(ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET).toNumber(), - ); - receiverData = ethUtil.bufferToHex(encodedReceiverData); - } - const erc721AssetData: ERC721AssetData = { + return { assetProxyId, - tokenAddress, - tokenId, - receiverData, + tokenAddress: ethUtil.addHexPrefix(tokenAddress), + tokenId: new BigNumber(tokenId.toString()), + receiverData: ethUtil.bufferToHex(receiverData), }; - return erc721AssetData; }, decodeAssetDataId(assetData: string): AssetProxyId { const encodedAssetData = ethUtil.toBuffer(assetData); - if (encodedAssetData.byteLength < 1) { + if (encodedAssetData.byteLength < constants.SELECTOR_LENGTH) { throw new Error( - `Could not decode Proxy Data. Expected length of encoded data to be at least 1. Got ${ + `Could not decode Proxy Data. Expected length of encoded data to be at least 4. Got ${ encodedAssetData.byteLength }`, ); } - const encodedAssetProxyId = encodedAssetData.slice(-1); + const encodedAssetProxyId = encodedAssetData.slice(0, constants.SELECTOR_LENGTH); const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); return assetProxyId; }, diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index ed5bd8101..383a657b8 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -5,4 +5,10 @@ export const constants = { // tslint:disable-next-line:custom-no-magic-numbers UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), TESTRPC_NETWORK_ID: 50, + ADDRESS_LENGTH: 20, + WORD_LENGTH: 32, + ERC20_ASSET_DATA_BYTE_LENGTH: 36, + ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH: 53, + SELECTOR_LENGTH: 4, + BASE_16: 16, }; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 70665a123..fa7d91347 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -152,18 +152,18 @@ export interface ECSignature { } export enum AssetProxyId { - INVALID, - ERC20, - ERC721, + INVALID = '0x00000000', + ERC20 = '0xf47261b0', + ERC721 = '0x08e937fa', } export interface ERC20AssetData { - assetProxyId: AssetProxyId; + assetProxyId: string; tokenAddress: string; } export interface ERC721AssetData { - assetProxyId: AssetProxyId; + assetProxyId: string; tokenAddress: string; tokenId: BigNumber; receiverData: string; |