diff options
8 files changed, 581 insertions, 27 deletions
diff --git a/packages/contracts/compiler.json b/packages/contracts/compiler.json index 48ba4ffcd..a11f2a2c0 100644 --- a/packages/contracts/compiler.json +++ b/packages/contracts/compiler.json @@ -22,6 +22,7 @@ "AssetProxyOwner", "DummyERC20Token", "DummyERC721Token", + "DummyERC721Receiver", "ERC20Proxy", "ERC721Proxy", "Exchange", @@ -30,6 +31,7 @@ "MultiSigWalletWithTimeLock", "TestAssetProxyDispatcher", "TestLibBytes", + "TestLibMem", "TestLibs", "TestSignatureValidator", "TokenRegistry", diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol index 2c321e134..017f94b1a 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol @@ -20,12 +20,14 @@ pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../utils/LibBytes/LibBytes.sol"; +import "../../utils/LibAssetProxyDecoder/LibAssetProxyDecoder.sol"; import "./MixinAssetProxy.sol"; import "./MixinAuthorizable.sol"; import "../../tokens/ERC20Token/IERC20Token.sol"; contract ERC20Proxy is LibBytes, + LibAssetProxyDecoder, MixinAssetProxy, MixinAuthorizable { @@ -34,34 +36,32 @@ contract ERC20Proxy is uint8 constant PROXY_ID = 1; /// @dev Internal version of `transferFrom`. - /// @param assetMetadata Encoded byte array. + /// @param proxyData Encoded byte array. /// @param from Address to transfer asset from. /// @param to Address to transfer asset to. /// @param amount Amount of asset to transfer. function transferFromInternal( - bytes memory assetMetadata, + bytes memory proxyData, address from, address to, uint256 amount ) internal { + // Decode proxy data. + ( + uint8 proxyId, + address token + ) = decodeERC20Data(proxyData); + // Data must be intended for this proxy. uint256 length = assetMetadata.length; require( - length == 21, - LENGTH_21_REQUIRED - ); - // TODO: Is this too inflexible in the future? - require( - uint8(assetMetadata[length - 1]) == PROXY_ID, - ASSET_PROXY_ID_MISMATCH + proxyId == PROXY_ID, + PROXY_ID_MISMATCH ); - // Decode metadata. - address token = readAddress(assetMetadata, 0); - // Transfer tokens. bool success = IERC20Token(token).transferFrom(from, to, amount); require( diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol index 07e01c774..f35e48eee 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol @@ -20,12 +20,14 @@ pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../utils/LibBytes/LibBytes.sol"; +import "../../utils/LibAssetProxyDecoder/LibAssetProxyDecoder.sol"; import "./MixinAssetProxy.sol"; import "./MixinAuthorizable.sol"; import "../../tokens/ERC721Token/ERC721Token.sol"; contract ERC721Proxy is LibBytes, + LibAssetProxyDecoder, MixinAssetProxy, MixinAuthorizable { @@ -33,19 +35,29 @@ contract ERC721Proxy is // Id of this proxy. uint8 constant PROXY_ID = 2; + string constant PROXY_ID_MISMATCH = "Proxy id in metadata does not match this proxy id."; + /// @dev Internal version of `transferFrom`. - /// @param assetMetadata Encoded byte array. + /// @param proxyData Encoded byte array. /// @param from Address to transfer asset from. /// @param to Address to transfer asset to. /// @param amount Amount of asset to transfer. function transferFromInternal( - bytes memory assetMetadata, + bytes memory proxyData, address from, address to, uint256 amount ) internal { + // Decode proxy data. + ( + uint8 proxyId, + address token, + uint256 tokenId, + bytes memory data + ) = decodeERC721Data(proxyData); + // Data must be intended for this proxy. uint256 length = assetMetadata.length; @@ -56,8 +68,8 @@ contract ERC721Proxy is // TODO: Is this too inflexible in the future? require( - uint8(assetMetadata[length - 1]) == PROXY_ID, - ASSET_PROXY_ID_MISMATCH + proxyId == PROXY_ID, + PROXY_ID_MISMATCH ); // There exists only 1 of each token. @@ -66,15 +78,9 @@ contract ERC721Proxy is INVALID_AMOUNT ); - // Decode metadata - address token = readAddress(assetMetadata, 0); - uint256 tokenId = readUint256(assetMetadata, 20); - // Transfer token. // Either succeeds or throws. - // @TODO: Call safeTransferFrom if there is additional - // data stored in `assetMetadata`. - ERC721Token(token).transferFrom(from, to, tokenId); + ERC721Token(token).safeTransferFrom(from, to, tokenId, data); } /// @dev Gets the proxy id associated with the proxy address. diff --git a/packages/contracts/src/contracts/current/test/DummyERC721Receiver/DummyERC721Receiver.sol b/packages/contracts/src/contracts/current/test/DummyERC721Receiver/DummyERC721Receiver.sol new file mode 100644 index 000000000..1596f3357 --- /dev/null +++ b/packages/contracts/src/contracts/current/test/DummyERC721Receiver/DummyERC721Receiver.sol @@ -0,0 +1,62 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016 Smart Contract Solutions, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +pragma solidity ^0.4.24; + +import "../../tokens/ERC721Token/IERC721Receiver.sol"; + +contract DummyERC721Receiver is + IERC721Receiver +{ + + event TokenReceived( + address from, + uint256 tokenId, + bytes data + ); + + /** + * @notice Handle the receipt of an NFT + * @dev The ERC721 smart contract calls this function on the recipient + * after a `safetransfer`. This function MAY throw to revert and reject the + * transfer. This function MUST use 50,000 gas or less. Return of other + * than the magic value MUST result in the transaction being reverted. + * Note: the contract address is always the message sender. + * @param _from The sending address + * @param _tokenId The NFT identifier which is being transfered + * @param _data Additional data with no specified format + * @return `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))` + */ + function onERC721Received( + address _from, + uint256 _tokenId, + bytes _data) + public + returns (bytes4) + { + emit TokenReceived(_from, _tokenId, _data); + return ERC721_RECEIVED; + } +} diff --git a/packages/contracts/src/contracts/current/test/TestLibMem/TestLibMem.sol b/packages/contracts/src/contracts/current/test/TestLibMem/TestLibMem.sol new file mode 100644 index 000000000..4cf62bf3a --- /dev/null +++ b/packages/contracts/src/contracts/current/test/TestLibMem/TestLibMem.sol @@ -0,0 +1,238 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.24; + +import "../../utils/LibMem/LibMem.sol"; +import "../../utils/LibBytes/LibBytes.sol"; + +contract TestLibMem is + LibMem, + LibBytes +{ + + function test1() + public + pure + { + // Length of array & length to copy + uint256 length = 0; + + // Create source array + bytes memory sourceArray = new bytes(length); + + // Create dest array with same contents as source array + bytes memory destArray = new bytes(length); + memcpy( + getMemAddress(destArray) + 32, // skip copying array length + getMemAddress(sourceArray) + 32, // skip copying array length + length + ); + + // Verify contents of source & dest arrays match + require( + areBytesEqual(sourceArray, destArray), + "Test #1 failed. Array contents are not the same." + ); + } + + function test2() + public + pure + { + // Length of array & length to copy + uint256 length = 1; + + // Create source array + bytes memory sourceArray = new bytes(length); + sourceArray[0] = byte(1); + + // Create dest array with same contents as source array + bytes memory destArray = new bytes(length); + memcpy( + getMemAddress(destArray) + 32, // skip copying array length + getMemAddress(sourceArray) + 32, // skip copying array length + length + ); + + // Verify contents of source & dest arrays match + require( + areBytesEqual(sourceArray, destArray), + "Test #2 failed. Array contents are not the same." + ); + } + + function test3() + public + pure + { + // Length of array & length to copy + uint256 length = 11; + + // Create source array + bytes memory sourceArray = new bytes(length); + for(uint256 i = 0; i < length; ++i) { + sourceArray[i] = byte((i % 0xF) + 1); // [1..f] + } + + // Create dest array with same contents as source array + bytes memory destArray = new bytes(length); + memcpy( + getMemAddress(destArray) + 32, // skip copying array length + getMemAddress(sourceArray) + 32, // skip copying array length + length + ); + + // Verify contents of source & dest arrays match + require( + areBytesEqual(sourceArray, destArray), + "Test #3 failed. Array contents are not the same." + ); + } + + function test4() + public + pure + { + // Length of array & length to copy + uint256 length = 32; + + // Create source array + bytes memory sourceArray = new bytes(length); + for(uint256 i = 0; i < length; ++i) { + sourceArray[i] = byte((i % 0xF) + 1); // [1..f] + } + + // Create dest array with same contents as source array + bytes memory destArray = new bytes(length); + memcpy( + getMemAddress(destArray) + 32, // skip copying array length + getMemAddress(sourceArray) + 32, // skip copying array length + length + ); + + // Verify contents of source & dest arrays match + require( + areBytesEqual(sourceArray, destArray), + "Test #4 failed. Array contents are not the same." + ); + } + + function test5() + public + pure + { + // Length of array & length to copy + uint256 length = 72; + + // Create source array + bytes memory sourceArray = new bytes(length); + for(uint256 i = 0; i < length; ++i) { + sourceArray[i] = byte((i % 0xF) + 1); // [1..f] + } + + // Create dest array with same contents as source array + bytes memory destArray = new bytes(length); + memcpy( + getMemAddress(destArray) + 32, // skip copying array length + getMemAddress(sourceArray) + 32, // skip copying array length + length + ); + + // Verify contents of source & dest arrays match + require( + areBytesEqual(sourceArray, destArray), + "Test #5 failed. Array contents are not the same." + ); + } + + + function test6() + public + pure + { + // Length of arrays + uint256 length1 = 72; + uint256 length2 = 100; + + // The full source array is used for comparisons at the end + bytes memory fullSourceArray = new bytes(length1 + length2); + + // First source array + bytes memory sourceArray1 = new bytes(length1); + for(uint256 i = 0; i < length1; ++i) { + sourceArray1[i] = byte((i % 0xF) + 1); // [1..f] + fullSourceArray[i] = byte((i % 0xF) + 1); // [1..f] + } + + // Second source array + bytes memory sourceArray2 = new bytes(length2); + for(uint256 j = 0; i < length2; ++i) { + sourceArray2[j] = byte((j % 0xF) + 1); // [1..f] + fullSourceArray[length1+j] = byte((j % 0xF) + 1); // [1..f] + } + + // Create dest array with same contents as source arrays + bytes memory destArray = new bytes(length1 + length2); + memcpy( + getMemAddress(destArray) + 32, // skip copying array length + getMemAddress(sourceArray1) + 32, // skip copying array length + length1 + ); + memcpy( + getMemAddress(destArray) + 32 + length1, // skip copying array length + sourceArray1 bytes + getMemAddress(sourceArray2) + 32, // skip copying array length + length2 + ); + + // Verify contents of source & dest arrays match + require( + areBytesEqual(fullSourceArray, destArray), + "Test #6 failed. Array contents are not the same." + ); + } + + function test7() + public + pure + { + // Length of array & length to copy + uint256 length = 72; + + // Create source array + bytes memory sourceArray = new bytes(length); + for(uint256 i = 0; i < length; ++i) { + sourceArray[i] = byte((i % 0xF) + 1); // [1..f] + } + + // Create dest array with same contents as source array + bytes memory destArray = new bytes(length); + memcpy( + getMemAddress(destArray) + 32, // skip copying array length + getMemAddress(sourceArray) + 32, // skip copying array length + length - 8 // Copy all but last byte. + ); + + // Verify contents of source & dest arrays match + // We expect this to fail + require( + areBytesEqual(sourceArray, destArray), + "Test #7 failed. Array contents are not the same." + ); + } +} diff --git a/packages/contracts/src/contracts/current/utils/LibAssetProxyDecoder/LibAssetProxyDecoder.sol b/packages/contracts/src/contracts/current/utils/LibAssetProxyDecoder/LibAssetProxyDecoder.sol new file mode 100644 index 000000000..ba53f2769 --- /dev/null +++ b/packages/contracts/src/contracts/current/utils/LibAssetProxyDecoder/LibAssetProxyDecoder.sol @@ -0,0 +1,74 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.24; +pragma experimental ABIEncoderV2; + +import "../LibBytes/LibBytes.sol"; + +contract LibAssetProxyDecoder is + LibBytes +{ + + string constant INVALID_ERC20_METADATA_LENGTH = "Metadata must have a length of 21."; + string constant INVALID_ERC721_METADATA_LENGTH = "Metadata must have a length of at least 53."; + + /// @dev Decodes ERC721 Asset Proxy data + function decodeERC20Data(bytes memory proxyData) + internal + pure + returns ( + uint8 proxyId, + address token + ) + { + require( + proxyData.length == 21, + INVALID_ERC20_METADATA_LENGTH + ); + proxyId = uint8(proxyData[0]); + token = readAddress(proxyData, 1); + + return (proxyId, token); + } + + /// @dev Decodes ERC721 Asset Proxy data + function decodeERC721Data(bytes memory proxyData) + internal + pure + returns ( + uint8 proxyId, + address token, + uint256 tokenId, + bytes memory data + ) + { + require( + proxyData.length >= 53, + INVALID_ERC721_METADATA_LENGTH + ); + proxyId = uint8(proxyData[0]); + token = readAddress(proxyData, 1); + tokenId = readUint256(proxyData, 21); + if (proxyData.length > 53) { + data = readBytes(proxyData, 53); + } + + return (proxyId, token, tokenId, data); + } +} diff --git a/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol b/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol index df2221c93..fb8359462 100644 --- a/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol +++ b/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol @@ -18,7 +18,11 @@ pragma solidity ^0.4.24; -contract LibBytes { +import "../LibMem/LibMem.sol"; + +contract LibBytes is + LibMem +{ // Revert reasons string constant GT_ZERO_LENGTH_REQUIRED = "Length must be greater than 0."; @@ -42,7 +46,7 @@ contract LibBytes { // Store last byte. result = b[b.length - 1]; - + assembly { // Decrement length of byte array. let newLen := sub(mload(b), 1) @@ -125,7 +129,7 @@ contract LibBytes { require( b.length >= index + 20, // 20 is length of address GTE_20_LENGTH_REQUIRED - ); + ); // Add offset to index: // 1. Arrays are prefixed by 32-byte length parameter (add 32 to index) @@ -157,7 +161,7 @@ contract LibBytes { require( b.length >= index + 20, // 20 is length of address GTE_20_LENGTH_REQUIRED - ); + ); // Add offset to index: // 1. Arrays are prefixed by 32-byte length parameter (add 32 to index) @@ -264,6 +268,7 @@ contract LibBytes { writeBytes32(b, index, bytes32(input)); } +======= /// @dev Reads the first 4 bytes from a byte array of arbitrary length. /// @param b Byte array to read first 4 bytes from. /// @return First 4 bytes of data. @@ -281,4 +286,67 @@ contract LibBytes { } return result; } + + /// @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 readBytes( + bytes memory b, + uint256 index + ) + internal + pure + returns (bytes memory result) + { + // Read length of nested bytes + require( + b.length >= index + 32, + GTE_32_LENGTH_REQUIRED + ); + uint256 nestedBytesLength = readUint256(b, index); + + // Assert length of <b> is valid, given + // length of nested bytes + require( + b.length >= index + 32 + nestedBytesLength, + GTE_32_LENGTH_REQUIRED + ); + + // Allocate memory and copy value to result + result = new bytes(nestedBytesLength); + memcpy( + getMemAddress(result) + 32, // +32 skips array length + getMemAddress(b) + index + 32, // +32 skips array length + nestedBytesLength + ); + + return result; + } + + /// @dev Writes a uint256 into a specific position in a byte array. + /// @param b Byte array to insert <input> into. + /// @param index Index in byte array of <input>. + /// @param input uint256 to put into byte array. + function writeBytes( + bytes memory b, + uint256 index, + bytes memory input + ) + internal + pure + { + // Read length of nested bytes + require( + b.length >= index + 32 /* 32 bytes to store length */ + input.length, + GTE_32_LENGTH_REQUIRED + ); + + // Copy <input> into <b> + memcpy( + getMemAddress(b) + index, + getMemAddress(input), + input.length + 32 /* 32 bytes to store length */ + ); + } } diff --git a/packages/contracts/src/contracts/current/utils/LibMem/LibMem.sol b/packages/contracts/src/contracts/current/utils/LibMem/LibMem.sol new file mode 100644 index 000000000..b07a5da54 --- /dev/null +++ b/packages/contracts/src/contracts/current/utils/LibMem/LibMem.sol @@ -0,0 +1,104 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.24; + +contract LibMem { + + function getMemAddress(bytes memory input) + internal + pure + returns (uint256 address_) + { + assembly { + address_ := input + } + return address_; + } + + /// @dev Writes a uint256 into a specific position in a byte array. + /// @param dest memory adress to copy bytes to + function memcpy( + uint256 dest, + uint256 source, + uint256 length + ) + internal + pure + { + // Base cases + if(length == 0) return; + if(source == dest) return; + + // Copy bytes from source to dest + assembly { + // Compute number of complete words to copy + remaining bytes + let lenFullWords := div(add(length, 0x1F), 0x20) + let remainder := mod(length, 0x20) + if gt(remainder, 0) { + lenFullWords := sub(lenFullWords, 1) + } + + // Copy full words from source to dest + let offset := 0 + let maxOffset := mul(0x20, lenFullWords) + for {offset := 0} lt(offset, maxOffset) {offset := add(offset, 0x20)} { + mstore(add(dest, offset), mload(add(source, offset))) + } + + // Copy remaining bytes + if gt(remainder, 0) { + // Read a full word from source, containing X bytes to copy to dest. + // We only want to keep the X bytes, zeroing out the remaining bytes. + // We accomplish this by a right shift followed by a left shift. + // Example: + // Suppose a word of 8 bits has all 1's: [11111111] + // Let X = 7 (we want to copy the first 7 bits) + // Apply a right shift of 1: [01111111] + // Apply a left shift of 1: [11111110] + let sourceShiftFactor := exp(2, mul(8, sub(0x20, remainder))) + let sourceWord := mload(add(source, offset)) + let sourceBytes := mul(div(sourceWord, sourceShiftFactor), sourceShiftFactor) + + // Read a full word from dest, containing (32-X) bytes to retain. + // We need to zero out the remaining bytes to be overwritten by source, + // while retaining the (32-X) bytes we don't want to overwrite. + // We accomplish this by a left shift followed by a right shift. + // Example: + // Suppose a word of 8 bits has all 1's: [11111111] + // Let X = 7 (we want to free the first 7 bits, and retain the last bit) + // Apply a left shift of 1: [11111110] + // Apply a right shift of 1: [01111111] + let destShiftFactor := exp(2, mul(8, remainder)) + let destWord := mload(add(dest, offset)) + let destBytes := div(mul(destWord, destShiftFactor), destShiftFactor) + + // Combine the source and dest bytes. There should be no overlap: + // The source bytes run from [0..X-1] and the dest bytes from [X..31]. + // Example: + // Following the example from above, we have [11111110] + // from the source word and [01111111] from the dest word. + // Combine these words using <or> to get [11111111]. + let combinedDestWord := or(sourceBytes, destBytes) + + // Store the combined word into dest + mstore(add(dest, offset), combinedDestWord) + } + } + } +} |