From 0de2b6983b7ab516d90a52499061b6dd6cab3bd5 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Mon, 29 Apr 2019 15:46:26 -0400 Subject: [PATCH] Add LibAssetData to contracts/asset-proxy (#1779) * Stop restarting node unnecesssarily during test * Add new, empty LibAssetData * Support encoding & decoding of ERC20 asset data * Support encoding & decoding of ERC721 asset data * Support encoding & decoding of ERC1155 asset data * Support encoding & decoding of multi-asset data * Support querying ERC20 balance from asset data * Support querying ERC721 balance from asset data * Support querying ERC1155 balance from asset data * Support querying balance from multi-asset data * Support querying ERC20 allowance from asset data * Support querying ERC721 allowance from asset data * Support querying ERC1155 allowance from asset data * In tests, wait for allowance set before checking * Introduce temporary variable `assetDataBody` * Handle edge case in multi-asset balance query * Support multi-asset allowance query by asset data * Move variable declaration up for readability. * Make all solhint-disable's cite specific rules And move the directives to the ends of lines whenever possible * Rename query tests to include " by asset data" * Extract test helper method * Extract another test helper method * Support batch queries of allowances & balances * In LibAssetData.sol, use IERC1155, not ...Mintable * Rename balance*() return vars: amount -> balance * Fix bug in ERC721 balance query Was using method balanceOf(), but needed to be using ownerOf(). getERC721TokenOwner() method lifted from @0x/extensions/contracts/src/OrderValidator/OrderValidator.sol * Reuse new en/decoders; avoid abi.decode(). * Start lowest allowance/balance from 0, not MAX_INT * Properly implement ERC1155 balance querying * Split lines for readability * Also check isApprovedForAll in 721 allowance query * Add neglected division of allowances by amounts * Rename methods: balanceOf -> getBalance * Rename methods: allowance -> getAllowance * Add methods: getBalanceAndAllowance() & batch...() * Rename return vars: amount -> allowance * Add devdoc comments * Rename batchGet* methods to getBatch* * Remove refactoring relic * Add revert messages to all require() calls * Reduce gas usage for ERC1155 asset data decoding * Don't use dockerized solc for ERC20 contracts Because they demand solc version 0.4.26, and it seems as though the tag for that version has been deleted from dockerhub. Without this, @0x/contracts-erc20 was failing to build. * Rename batch functions to use plurals * Skip dockerized solc for contracts needing 0.4.26 I seems as though the tag for that version has been deleted from dockerhub. Without this, these contracts were failing to build. * Make revert reasons follow snake case convention --- contracts/asset-proxy/compiler.json | 3 +- .../contracts/src/libs/LibAssetData.sol | 420 +++++++++++++++++ contracts/asset-proxy/package.json | 2 +- contracts/asset-proxy/src/artifacts.ts | 4 +- contracts/asset-proxy/src/wrappers.ts | 1 + contracts/asset-proxy/test/lib_asset_data.ts | 426 ++++++++++++++++++ contracts/asset-proxy/tsconfig.json | 1 + contracts/coordinator/compiler.json | 2 +- contracts/erc20/compiler.json | 2 +- contracts/exchange-forwarder/compiler.json | 2 +- contracts/extensions/compiler.json | 2 +- contracts/multisig/compiler.json | 2 +- contracts/utils/test/lib_address_array.ts | 10 +- 13 files changed, 860 insertions(+), 17 deletions(-) create mode 100644 contracts/asset-proxy/contracts/src/libs/LibAssetData.sol create mode 100644 contracts/asset-proxy/test/lib_asset_data.ts diff --git a/contracts/asset-proxy/compiler.json b/contracts/asset-proxy/compiler.json index 6bf9c16c56..d583fdc7cf 100644 --- a/contracts/asset-proxy/compiler.json +++ b/contracts/asset-proxy/compiler.json @@ -30,6 +30,7 @@ "src/MultiAssetProxy.sol", "src/interfaces/IAssetData.sol", "src/interfaces/IAssetProxy.sol", - "src/interfaces/IAuthorizable.sol" + "src/interfaces/IAuthorizable.sol", + "src/libs/LibAssetData.sol" ] } diff --git a/contracts/asset-proxy/contracts/src/libs/LibAssetData.sol b/contracts/asset-proxy/contracts/src/libs/LibAssetData.sol new file mode 100644 index 0000000000..ebde49b477 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/libs/LibAssetData.sol @@ -0,0 +1,420 @@ +/* + + Copyright 2019 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.5.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/LibBytes.sol"; +import "@0x/contracts-erc1155/contracts/src/interfaces/IERC1155.sol"; +import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; +import "@0x/contracts-erc721/contracts/src/interfaces/IERC721Token.sol"; + + +library LibAssetData { + bytes4 constant public ERC20_PROXY_ID = bytes4(keccak256("ERC20Token(address)")); + bytes4 constant public ERC721_PROXY_ID = bytes4(keccak256("ERC721Token(address,uint256)")); + bytes4 constant public ERC1155_PROXY_ID = bytes4(keccak256("ERC1155Assets(address,uint256[],uint256[],bytes)")); + bytes4 constant public MULTI_ASSET_PROXY_ID = bytes4(keccak256("MultiAsset(uint256[],bytes[])")); + + /// @dev Returns the owner's balance of the token(s) specified in + /// assetData. When the asset data contains multiple tokens (eg in + /// ERC1155 or Multi-Asset), the return value indicates how many + /// complete "baskets" of those tokens are owned by owner. + /// @param owner Owner of the tokens specified by assetData. + /// @param assetData Description of tokens, per the AssetProxy contract + /// specification. + /// @return Number of tokens (or token baskets) held by owner. + function getBalance(address owner, bytes memory assetData) + public + view + returns (uint256 balance) + { + bytes4 proxyId = LibBytes.readBytes4(assetData, 0); + if (proxyId == ERC20_PROXY_ID) { + address tokenAddress = LibBytes.readAddress(assetData, 16); + return IERC20Token(tokenAddress).balanceOf(owner); + } else if (proxyId == ERC721_PROXY_ID) { + (, address tokenAddress, uint256 tokenId) = decodeERC721AssetData(assetData); + return getERC721TokenOwner(tokenAddress, tokenId) == owner ? 1 : 0; + } else if (proxyId == ERC1155_PROXY_ID) { + uint256 lowestTokenBalance = 0; + ( + , + address tokenAddress, + uint256[] memory tokenIds, + uint256[] memory tokenValues, + ) = decodeERC1155AssetData(assetData); + for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 tokenBalance = IERC1155(tokenAddress).balanceOf(owner, tokenIds[i]) / tokenValues[i]; + if (tokenBalance < lowestTokenBalance || lowestTokenBalance == 0) { + lowestTokenBalance = tokenBalance; + } + } + return lowestTokenBalance; + } else if (proxyId == MULTI_ASSET_PROXY_ID) { + uint256 lowestAssetBalance = 0; + (, uint256[] memory assetAmounts, bytes[] memory nestedAssetData) = decodeMultiAssetData(assetData); + for (uint256 i = 0; i < nestedAssetData.length; i++) { + uint256 assetBalance = getBalance(owner, nestedAssetData[i]) / assetAmounts[i]; + if (assetBalance < lowestAssetBalance || lowestAssetBalance == 0) { + lowestAssetBalance = assetBalance; + } + } + return lowestAssetBalance; + } else { + revert("UNSUPPORTED_PROXY_IDENTIFIER"); + } + } + + /// @dev Calls getBalance() for each element of assetData. + /// @param owner Owner of the tokens specified by assetData. + /// @param assetData Array of token descriptors, each encoded per the + /// AssetProxy contract specification. + /// @return Array of token balances from getBalance(), with each element + /// corresponding to the same-indexed element in the assetData input. + function getBatchBalances(address owner, bytes[] memory assetData) + public + view + returns (uint256[] memory balances) + { + balances = new uint256[](assetData.length); + for (uint256 i = 0; i < assetData.length; i++) { + balances[i] = getBalance(owner, assetData[i]); + } + } + + /// @dev Returns the number of token(s) (described by assetData) that + /// spender is authorized to spend. When the asset data contains + /// multiple tokens (eg for Multi-Asset), the return value indicates + /// how many complete "baskets" of those tokens may be spent by spender. + /// @param owner Owner of the tokens specified by assetData. + /// @param spender Address whose authority to spend is in question. + /// @param assetData Description of tokens, per the AssetProxy contract + /// specification. + /// @return Number of tokens (or token baskets) that the spender is + /// authorized to spend. + function getAllowance(address owner, address spender, bytes memory assetData) + public + view + returns (uint256 allowance) + { + bytes4 proxyId = LibBytes.readBytes4(assetData, 0); + + if (proxyId == ERC20_PROXY_ID) { + address tokenAddress = LibBytes.readAddress(assetData, 16); + return IERC20Token(tokenAddress).allowance(owner, spender); + } else if (proxyId == ERC721_PROXY_ID) { + (, address tokenAddress, uint256 tokenId) = decodeERC721AssetData(assetData); + IERC721Token token = IERC721Token(tokenAddress); + if (spender == token.getApproved(tokenId) || token.isApprovedForAll(owner, spender)) { + return 1; + } else { + return 0; + } + } else if (proxyId == ERC1155_PROXY_ID) { + (, address tokenAddress, , , ) = decodeERC1155AssetData(assetData); + if (IERC1155(tokenAddress).isApprovedForAll(owner, spender)) { + return 1; + } else { + return 0; + } + } else if (proxyId == MULTI_ASSET_PROXY_ID) { + uint256 lowestAssetAllowance = 0; + // solhint-disable-next-line indent + (, uint256[] memory amounts, bytes[] memory nestedAssetData) = decodeMultiAssetData(assetData); + for (uint256 i = 0; i < nestedAssetData.length; i++) { + uint256 assetAllowance = getAllowance(owner, spender, nestedAssetData[i]) / amounts[i]; + if (assetAllowance < lowestAssetAllowance || lowestAssetAllowance == 0) { + lowestAssetAllowance = assetAllowance; + } + } + return lowestAssetAllowance; + } else { + revert("UNSUPPORTED_PROXY_IDENTIFIER"); + } + } + + /// @dev Calls getAllowance() for each element of assetData. + /// @param owner Owner of the tokens specified by assetData. + /// @param spender Address whose authority to spend is in question. + /// @param assetData Description of tokens, per the AssetProxy contract + /// specification. + /// @return An array of token allowances from getAllowance(), with each + /// element corresponding to the same-indexed element in the assetData + /// input. + function getBatchAllowances(address owner, address spender, bytes[] memory assetData) + public + view + returns (uint256[] memory allowances) + { + allowances = new uint256[](assetData.length); + for (uint256 i = 0; i < assetData.length; i++) { + allowances[i] = getAllowance(owner, spender, assetData[i]); + } + } + + /// @dev Calls getBalance() and getAllowance() for assetData. + /// @param owner Owner of the tokens specified by assetData. + /// @param spender Address whose authority to spend is in question. + /// @param assetData Description of tokens, per the AssetProxy contract + /// specification. + /// @return Number of tokens (or token baskets) held by owner, and number + /// of tokens (or token baskets) that the spender is authorized to + /// spend. + function getBalanceAndAllowance(address owner, address spender, bytes memory assetData) + public + view + returns (uint256 balance, uint256 allowance) + { + balance = getBalance(owner, assetData); + allowance = getAllowance(owner, spender, assetData); + } + + /// @dev Calls getBatchBalances() and getBatchAllowances() for each element + /// of assetData. + /// @param owner Owner of the tokens specified by assetData. + /// @param spender Address whose authority to spend is in question. + /// @param assetData Description of tokens, per the AssetProxy contract + /// specification. + /// @return An array of token balances from getBalance(), and an array of + /// token allowances from getAllowance(), with each element + /// corresponding to the same-indexed element in the assetData input. + function getBatchBalancesAndAllowances(address owner, address spender, bytes[] memory assetData) + public + view + returns (uint256[] memory balances, uint256[] memory allowances) + { + balances = getBatchBalances(owner, assetData); + allowances = getBatchAllowances(owner, spender, assetData); + } + + /// @dev Encode ERC-20 asset data into the format described in the + /// AssetProxy contract specification. + /// @param tokenAddress The address of the ERC-20 contract hosting the + /// token to be traded. + /// @return AssetProxy-compliant data describing the asset. + function encodeERC20AssetData(address tokenAddress) + public + pure + returns (bytes memory assetData) + { + return abi.encodeWithSelector(ERC20_PROXY_ID, tokenAddress); + } + + /// @dev Decode ERC-20 asset data from the format described in the + /// AssetProxy contract specification. + /// @param assetData AssetProxy-compliant asset data describing an ERC-20 + /// asset. + /// @return The ERC-20 AssetProxy identifier, and the address of the ERC-20 + /// contract hosting this asset. + function decodeERC20AssetData(bytes memory assetData) + public + pure + returns ( + bytes4 proxyId, + address tokenAddress + ) + { + proxyId = LibBytes.readBytes4(assetData, 0); + + require(proxyId == ERC20_PROXY_ID, "WRONG_PROXY_ID"); + + tokenAddress = LibBytes.readAddress(assetData, 16); + } + + /// @dev Encode ERC-721 asset data into the format described in the + /// AssetProxy specification. + /// @param tokenAddress The address of the ERC-721 contract hosting the + /// token to be traded. + /// @param tokenId The identifier of the specific token to be traded. + /// @return AssetProxy-compliant asset data describing the asset. + function encodeERC721AssetData( + address tokenAddress, + uint256 tokenId + ) + public + pure + returns (bytes memory assetData) + { + return abi.encodeWithSelector(ERC721_PROXY_ID, tokenAddress, tokenId); + } + + /// @dev Decode ERC-721 asset data from the format described in the + /// AssetProxy contract specification. + /// @param assetData AssetProxy-compliant asset data describing an ERC-721 + /// asset. + /// @return The ERC-721 AssetProxy identifier, the address of the ERC-721 + /// contract hosting this asset, and the identifier of the specific + /// token to be traded. + function decodeERC721AssetData(bytes memory assetData) + public + pure + returns ( + bytes4 proxyId, + address tokenAddress, + uint256 tokenId + ) + { + proxyId = LibBytes.readBytes4(assetData, 0); + + require(proxyId == ERC721_PROXY_ID, "WRONG_PROXY_ID"); + + tokenAddress = LibBytes.readAddress(assetData, 16); + tokenId = LibBytes.readUint256(assetData, 36); + } + + /// @dev Encode ERC-1155 asset data into the format described in the + /// AssetProxy contract specification. + /// @param tokenAddress The address of the ERC-1155 contract hosting the + /// token(s) to be traded. + /// @param tokenIds The identifiers of the specific tokens to be traded. + /// @param tokenValues The amounts of each token to be traded. + /// @param callbackData ... + /// @return AssetProxy-compliant asset data describing the set of assets. + function encodeERC1155AssetData( + address tokenAddress, + uint256[] memory tokenIds, + uint256[] memory tokenValues, + bytes memory callbackData + ) + public + pure + returns (bytes memory assetData) + { + return abi.encodeWithSelector(ERC1155_PROXY_ID, tokenAddress, tokenIds, tokenValues, callbackData); + } + + /// @dev Decode ERC-1155 asset data from the format described in the + /// AssetProxy contract specification. + /// @param assetData AssetProxy-compliant asset data describing an ERC-1155 + /// set of assets. + /// @return The ERC-1155 AssetProxy identifier, the address of the ERC-1155 + /// contract hosting the assets, an array of the identifiers of the + /// tokens to be traded, an array of token amounts to be traded, and + /// callback data. Each element of the arrays corresponds to the + /// same-indexed element of the other array. Return values specified as + /// `memory` are returned as pointers to locations within the memory of + /// the input parameter `assetData`. + function decodeERC1155AssetData(bytes memory assetData) + public + pure + returns ( + bytes4 proxyId, + address tokenAddress, + uint256[] memory tokenIds, + uint256[] memory tokenValues, + bytes memory callbackData + ) + { + proxyId = LibBytes.readBytes4(assetData, 0); + + require(proxyId == ERC1155_PROXY_ID, "WRONG_PROXY_ID"); + + assembly { + // Skip selector and length to get to the first parameter: + assetData := add(assetData, 36) + // Read the value of the first parameter: + tokenAddress := mload(assetData) + // Point to the next parameter's data: + tokenIds := add(assetData, mload(add(assetData, 32))) + // Point to the next parameter's data: + tokenValues := add(assetData, mload(add(assetData, 64))) + // Point to the next parameter's data: + callbackData := add(assetData, mload(add(assetData, 96))) + } + } + + /// @dev Encode data for multiple assets, per the AssetProxy contract + /// specification. + /// @param amounts The amounts of each asset to be traded. + /// @param nestedAssetData AssetProxy-compliant data describing each asset + /// to be traded. + /// @return AssetProxy-compliant data describing the set of assets. + function encodeMultiAssetData(uint256[] memory amounts, bytes[] memory nestedAssetData) + public + pure + returns (bytes memory assetData) + { + assetData = abi.encodeWithSelector(MULTI_ASSET_PROXY_ID, amounts, nestedAssetData); + } + + /// @dev Decode multi-asset data from the format described in the + /// AssetProxy contract specification. + /// @param assetData AssetProxy-compliant data describing a multi-asset + /// basket. + /// @return The Multi-Asset AssetProxy identifier, an array of the amounts + /// of the assets to be traded, and an array of the + /// AssetProxy-compliant data describing each asset to be traded. Each + /// element of the arrays corresponds to the same-indexed element of + /// the other array. + function decodeMultiAssetData(bytes memory assetData) + public + pure + returns ( + bytes4 proxyId, + uint256[] memory amounts, + bytes[] memory nestedAssetData + ) + { + proxyId = LibBytes.readBytes4(assetData, 0); + + require(proxyId == MULTI_ASSET_PROXY_ID, "WRONG_PROXY_ID"); + + // solhint-disable-next-line indent + (amounts, nestedAssetData) = abi.decode(LibBytes.slice(assetData, 4, assetData.length), (uint256[], bytes[])); + } + + /// @dev Calls `token.ownerOf(tokenId)`, but returns a null owner instead of reverting on an unowned token. + /// @param token Address of ERC721 token. + /// @param tokenId The identifier for the specific NFT. + /// @return Owner of tokenId or null address if unowned. + function getERC721TokenOwner(address token, uint256 tokenId) + public + view + returns (address owner) + { + assembly { + // load free memory pointer + let cdStart := mload(64) + + // bytes4(keccak256(ownerOf(uint256))) = 0x6352211e + mstore(cdStart, 0x6352211e00000000000000000000000000000000000000000000000000000000) + mstore(add(cdStart, 4), tokenId) + + // staticcall `ownerOf(tokenId)` + // `ownerOf` will revert if tokenId is not owned + let success := staticcall( + gas, // forward all gas + token, // call token contract + cdStart, // start of calldata + 36, // length of input is 36 bytes + cdStart, // write output over input + 32 // size of output is 32 bytes + ) + + // Success implies that tokenId is owned + // Copy owner from return data if successful + if success { + owner := mload(cdStart) + } + } + + // Owner initialized to address(0), no need to modify if call is unsuccessful + return owner; + } +} diff --git a/contracts/asset-proxy/package.json b/contracts/asset-proxy/package.json index 445877654c..633f0e70ed 100644 --- a/contracts/asset-proxy/package.json +++ b/contracts/asset-proxy/package.json @@ -34,7 +34,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "./generated-artifacts/@(ERC1155Proxy|ERC20Proxy|ERC721Proxy|IAssetData|IAssetProxy|IAuthorizable|MixinAuthorizable|MultiAssetProxy).json", + "abis": "./generated-artifacts/@(ERC1155Proxy|ERC20Proxy|ERC721Proxy|IAssetData|IAssetProxy|IAuthorizable|LibAssetData|MixinAuthorizable|MultiAssetProxy).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/asset-proxy/src/artifacts.ts b/contracts/asset-proxy/src/artifacts.ts index 0dc768bc11..9c82fe54ff 100644 --- a/contracts/asset-proxy/src/artifacts.ts +++ b/contracts/asset-proxy/src/artifacts.ts @@ -11,12 +11,14 @@ import * as ERC721Proxy from '../generated-artifacts/ERC721Proxy.json'; import * as IAssetData from '../generated-artifacts/IAssetData.json'; import * as IAssetProxy from '../generated-artifacts/IAssetProxy.json'; import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json'; +import * as LibAssetData from '../generated-artifacts/LibAssetData.json'; import * as MixinAuthorizable from '../generated-artifacts/MixinAuthorizable.json'; import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; export const artifacts = { + LibAssetData: LibAssetData as ContractArtifact, + ERC1155Proxy: ERC1155Proxy as ContractArtifact, ERC20Proxy: ERC20Proxy as ContractArtifact, ERC721Proxy: ERC721Proxy as ContractArtifact, - ERC1155Proxy: ERC1155Proxy as ContractArtifact, MixinAuthorizable: MixinAuthorizable as ContractArtifact, MultiAssetProxy: MultiAssetProxy as ContractArtifact, IAssetData: IAssetData as ContractArtifact, diff --git a/contracts/asset-proxy/src/wrappers.ts b/contracts/asset-proxy/src/wrappers.ts index 3412cb0266..2b7bf8bbde 100644 --- a/contracts/asset-proxy/src/wrappers.ts +++ b/contracts/asset-proxy/src/wrappers.ts @@ -9,5 +9,6 @@ export * from '../generated-wrappers/erc721_proxy'; export * from '../generated-wrappers/i_asset_data'; export * from '../generated-wrappers/i_asset_proxy'; export * from '../generated-wrappers/i_authorizable'; +export * from '../generated-wrappers/lib_asset_data'; export * from '../generated-wrappers/mixin_authorizable'; export * from '../generated-wrappers/multi_asset_proxy'; diff --git a/contracts/asset-proxy/test/lib_asset_data.ts b/contracts/asset-proxy/test/lib_asset_data.ts new file mode 100644 index 0000000000..bd2b4dcd6d --- /dev/null +++ b/contracts/asset-proxy/test/lib_asset_data.ts @@ -0,0 +1,426 @@ +// TODO: change test titles to say "... from asset data" +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; + +import { + artifacts as erc1155Artifacts, + ERC1155MintableContract, + ERC1155TransferSingleEventArgs, + IERC1155MintableContract, +} from '@0x/contracts-erc1155'; +import { artifacts as erc20Artifacts, DummyERC20TokenContract, IERC20TokenContract } from '@0x/contracts-erc20'; +import { artifacts as erc721Artifacts, DummyERC721TokenContract, IERC721TokenContract } from '@0x/contracts-erc721'; +import { chaiSetup, constants, LogDecoder, provider, txDefaults, web3Wrapper } from '@0x/contracts-test-utils'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { AssetProxyId } from '@0x/types'; +import { BigNumber } from '@0x/utils'; + +import { artifacts, LibAssetDataContract } from '../src'; + +chaiSetup.configure(); +const expect = chai.expect; + +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +const KNOWN_ERC20_ENCODING = { + address: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', + assetData: '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48', +}; +const KNOWN_ERC721_ENCODING = { + address: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', + tokenId: new BigNumber(1), + assetData: + '0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001', +}; +const KNOWN_ERC1155_ENCODING = { + tokenAddress: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', + tokenIds: [new BigNumber(100), new BigNumber(1001), new BigNumber(10001)], + tokenValues: [new BigNumber(200), new BigNumber(2001), new BigNumber(20001)], + callbackData: + '0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001', + assetData: + '0xa7cb5fb70000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000003e90000000000000000000000000000000000000000000000000000000000002711000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000007d10000000000000000000000000000000000000000000000000000000000004e210000000000000000000000000000000000000000000000000000000000000044025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000', +}; +const KNOWN_MULTI_ASSET_ENCODING = { + amounts: [new BigNumber(70), new BigNumber(1), new BigNumber(18)], + nestedAssetData: [ + KNOWN_ERC20_ENCODING.assetData, + KNOWN_ERC721_ENCODING.assetData, + KNOWN_ERC1155_ENCODING.assetData, + ], + assetData: + '0x94cfcdd7000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000024f47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000204a7cb5fb70000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000003e90000000000000000000000000000000000000000000000000000000000002711000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000007d10000000000000000000000000000000000000000000000000000000000004e210000000000000000000000000000000000000000000000000000000000000044025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c4800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', +}; + +describe('LibAssetData', () => { + let libAssetData: LibAssetDataContract; + + let tokenOwnerAddress: string; + let approvedSpenderAddress: string; + let anotherApprovedSpenderAddress: string; + + let erc20TokenAddress: string; + const erc20TokenTotalSupply = new BigNumber(1); + + let erc721TokenAddress: string; + const firstERC721TokenId = new BigNumber(1); + const numberOfERC721Tokens = 10; + + let erc1155MintableAddress: string; + let erc1155TokenId: BigNumber; + + before(async () => { + await blockchainLifecycle.startAsync(); + + libAssetData = await LibAssetDataContract.deployFrom0xArtifactAsync( + artifacts.LibAssetData, + provider, + txDefaults, + ); + + [ + tokenOwnerAddress, + approvedSpenderAddress, + anotherApprovedSpenderAddress, + ] = await web3Wrapper.getAvailableAddressesAsync(); + + erc20TokenAddress = (await DummyERC20TokenContract.deployFrom0xArtifactAsync( + erc20Artifacts.DummyERC20Token, + provider, + txDefaults, + 'Dummy', + 'DUM', + new BigNumber(1), + erc20TokenTotalSupply, + )).address; + + const erc721TokenContract = await DummyERC721TokenContract.deployFrom0xArtifactAsync( + erc721Artifacts.DummyERC721Token, + provider, + txDefaults, + 'Dummy', + 'DUM', + ); + erc721TokenAddress = erc721TokenContract.address; + // mint `numberOfERC721Tokens` tokens + const transactionMinedPromises = []; + for (let i = 0; i < numberOfERC721Tokens; i++) { + transactionMinedPromises.push( + web3Wrapper.awaitTransactionSuccessAsync( + await erc721TokenContract.mint.sendTransactionAsync( + tokenOwnerAddress, + firstERC721TokenId.plus(i - 1), + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ), + ); + } + await Promise.all(transactionMinedPromises); + + const erc1155MintableContract = await ERC1155MintableContract.deployFrom0xArtifactAsync( + erc1155Artifacts.ERC1155Mintable, + provider, + txDefaults, + ); + erc1155MintableAddress = erc1155MintableContract.address; + // Somewhat re-inventing the wheel here, but the prior art currently + // exists only as an unexported test util in the erc1155 package + // (Erc1155Wrapper.mintFungibleTokensAsync() in erc1155/test/utils/). + // This is concise enough to justify duplication, but it sure is ugly. + // tslint:disable-next-line no-unnecessary-type-assertion + erc1155TokenId = ((await new LogDecoder(web3Wrapper, erc1155Artifacts).getTxWithDecodedLogsAsync( + await erc1155MintableContract.create.sendTransactionAsync('uri:Dummy', /*isNonFungible:*/ false), + )).logs[0] as LogWithDecodedArgs).args.id; + await web3Wrapper.awaitTransactionSuccessAsync( + await erc1155MintableContract.mintFungible.sendTransactionAsync( + erc1155TokenId, + [tokenOwnerAddress], + [new BigNumber(1)], + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + }); + + async function setERC20AllowanceAsync(): Promise { + return web3Wrapper.awaitTransactionSuccessAsync( + await new IERC20TokenContract( + erc20Artifacts.IERC20Token.compilerOutput.abi, + erc20TokenAddress, + provider, + ).approve.sendTransactionAsync(approvedSpenderAddress, new BigNumber(1), { from: tokenOwnerAddress }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + + async function setERC721AllowanceAsync(): Promise { + return web3Wrapper.awaitTransactionSuccessAsync( + await new IERC721TokenContract( + erc721Artifacts.IERC721Token.compilerOutput.abi, + erc721TokenAddress, + provider, + ).approve.sendTransactionAsync(approvedSpenderAddress, new BigNumber(1), { from: tokenOwnerAddress }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + + it('should have a deployed-to address', () => { + expect(libAssetData.address.slice(0, 2)).to.equal('0x'); + }); + + it('should encode ERC20 asset data', async () => { + expect(await libAssetData.encodeERC20AssetData.callAsync(KNOWN_ERC20_ENCODING.address)).to.equal( + KNOWN_ERC20_ENCODING.assetData, + ); + }); + + it('should decode ERC20 asset data', async () => { + expect(await libAssetData.decodeERC20AssetData.callAsync(KNOWN_ERC20_ENCODING.assetData)).to.deep.equal([ + AssetProxyId.ERC20, + KNOWN_ERC20_ENCODING.address, + ]); + }); + + it('should encode ERC721 asset data', async () => { + expect( + await libAssetData.encodeERC721AssetData.callAsync( + KNOWN_ERC721_ENCODING.address, + KNOWN_ERC721_ENCODING.tokenId, + ), + ).to.equal(KNOWN_ERC721_ENCODING.assetData); + }); + + it('should decode ERC721 asset data', async () => { + expect(await libAssetData.decodeERC721AssetData.callAsync(KNOWN_ERC721_ENCODING.assetData)).to.deep.equal([ + AssetProxyId.ERC721, + KNOWN_ERC721_ENCODING.address, + KNOWN_ERC721_ENCODING.tokenId, + ]); + }); + + it('should encode ERC1155 asset data', async () => { + expect( + await libAssetData.encodeERC1155AssetData.callAsync( + KNOWN_ERC1155_ENCODING.tokenAddress, + KNOWN_ERC1155_ENCODING.tokenIds, + KNOWN_ERC1155_ENCODING.tokenValues, + KNOWN_ERC1155_ENCODING.callbackData, + ), + ).to.equal(KNOWN_ERC1155_ENCODING.assetData); + }); + + it('should decode ERC1155 asset data', async () => { + expect(await libAssetData.decodeERC1155AssetData.callAsync(KNOWN_ERC1155_ENCODING.assetData)).to.deep.equal([ + AssetProxyId.ERC1155, + KNOWN_ERC1155_ENCODING.tokenAddress, + KNOWN_ERC1155_ENCODING.tokenIds, + KNOWN_ERC1155_ENCODING.tokenValues, + KNOWN_ERC1155_ENCODING.callbackData, + ]); + }); + + it('should encode multiasset data', async () => { + expect( + await libAssetData.encodeMultiAssetData.callAsync( + KNOWN_MULTI_ASSET_ENCODING.amounts, + KNOWN_MULTI_ASSET_ENCODING.nestedAssetData, + ), + ).to.equal(KNOWN_MULTI_ASSET_ENCODING.assetData); + }); + + it('should decode multiasset data', async () => { + expect(await libAssetData.decodeMultiAssetData.callAsync(KNOWN_MULTI_ASSET_ENCODING.assetData)).to.deep.equal([ + AssetProxyId.MultiAsset, + KNOWN_MULTI_ASSET_ENCODING.amounts, + KNOWN_MULTI_ASSET_ENCODING.nestedAssetData, + ]); + }); + + it('should query ERC20 balance by asset data', async () => { + expect( + await libAssetData.getBalance.callAsync( + tokenOwnerAddress, + await libAssetData.encodeERC20AssetData.callAsync(erc20TokenAddress), + ), + ).to.bignumber.equal(erc20TokenTotalSupply); + }); + + it('should query ERC721 balance by asset data', async () => { + expect( + await libAssetData.getBalance.callAsync( + tokenOwnerAddress, + await libAssetData.encodeERC721AssetData.callAsync(erc721TokenAddress, firstERC721TokenId), + ), + ).to.bignumber.equal(1); + }); + + it('should query ERC1155 balances by asset data', async () => { + expect( + await libAssetData.getBalance.callAsync( + tokenOwnerAddress, + await libAssetData.encodeERC1155AssetData.callAsync( + erc1155MintableAddress, + [erc1155TokenId], + [new BigNumber(1)], // token values + '0x', // callback data + ), + ), + ).to.bignumber.equal(1); + }); + + it('should query multi-asset batch balance by asset data', async () => { + expect( + await libAssetData.getBalance.callAsync( + tokenOwnerAddress, + await libAssetData.encodeMultiAssetData.callAsync( + [new BigNumber(1), new BigNumber(1)], + [ + await libAssetData.encodeERC20AssetData.callAsync(erc20TokenAddress), + await libAssetData.encodeERC721AssetData.callAsync(erc721TokenAddress, firstERC721TokenId), + ], + ), + ), + ).to.bignumber.equal(Math.min(erc20TokenTotalSupply.toNumber(), numberOfERC721Tokens)); + }); + + it('should query ERC20 allowances by asset data', async () => { + await setERC20AllowanceAsync(); + expect( + await libAssetData.getAllowance.callAsync( + tokenOwnerAddress, + approvedSpenderAddress, + await libAssetData.encodeERC20AssetData.callAsync(erc20TokenAddress), + ), + ).to.bignumber.equal(1); + }); + + it('should query ERC721 approval by asset data', async () => { + await setERC721AllowanceAsync(); + expect( + await libAssetData.getAllowance.callAsync( + tokenOwnerAddress, + approvedSpenderAddress, + await libAssetData.encodeERC721AssetData.callAsync(erc721TokenAddress, firstERC721TokenId), + ), + ).to.bignumber.equal(1); + }); + + it('should query ERC721 approvalForAll by assetData', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await new IERC721TokenContract( + erc721Artifacts.IERC721Token.compilerOutput.abi, + erc721TokenAddress, + provider, + ).setApprovalForAll.sendTransactionAsync(anotherApprovedSpenderAddress, true, { + from: tokenOwnerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + expect( + await libAssetData.getAllowance.callAsync( + tokenOwnerAddress, + anotherApprovedSpenderAddress, + await libAssetData.encodeERC721AssetData.callAsync(erc721TokenAddress, firstERC721TokenId), + ), + ).to.bignumber.equal(1); + }); + + it('should query ERC1155 allowances by asset data', async () => { + await web3Wrapper.awaitTransactionSuccessAsync( + await new IERC1155MintableContract( + erc1155Artifacts.IERC1155Mintable.compilerOutput.abi, + erc1155MintableAddress, + provider, + ).setApprovalForAll.sendTransactionAsync(approvedSpenderAddress, true, { from: tokenOwnerAddress }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + expect( + await libAssetData.getAllowance.callAsync( + tokenOwnerAddress, + approvedSpenderAddress, + await libAssetData.encodeERC1155AssetData.callAsync( + erc1155MintableAddress, + [erc1155TokenId], + [new BigNumber(1)], + '0x', + ), + ), + ).to.bignumber.equal(1); + }); + + it('should query multi-asset allowances by asset data', async () => { + await setERC20AllowanceAsync(); + await setERC721AllowanceAsync(); + expect( + await libAssetData.getAllowance.callAsync( + tokenOwnerAddress, + approvedSpenderAddress, + await libAssetData.encodeMultiAssetData.callAsync( + [new BigNumber(1), new BigNumber(1)], + [ + await libAssetData.encodeERC20AssetData.callAsync(erc20TokenAddress), + await libAssetData.encodeERC721AssetData.callAsync(erc721TokenAddress, firstERC721TokenId), + ], + ), + ), + ).to.bignumber.equal(1); + return; + }); + + it('should query balances for a batch of asset data strings', async () => { + expect( + await libAssetData.getBatchBalances.callAsync(tokenOwnerAddress, [ + await libAssetData.encodeERC20AssetData.callAsync(erc20TokenAddress), + await libAssetData.encodeERC721AssetData.callAsync(erc721TokenAddress, firstERC721TokenId), + ]), + ).to.deep.equal([new BigNumber(erc20TokenTotalSupply), new BigNumber(1)]); + }); + + it('should query allowances for a batch of asset data strings', async () => { + await setERC20AllowanceAsync(); + await setERC721AllowanceAsync(); + expect( + await libAssetData.getBatchAllowances.callAsync(tokenOwnerAddress, approvedSpenderAddress, [ + await libAssetData.encodeERC20AssetData.callAsync(erc20TokenAddress), + await libAssetData.encodeERC721AssetData.callAsync(erc721TokenAddress, firstERC721TokenId), + ]), + ).to.deep.equal([new BigNumber(1), new BigNumber(1)]); + }); + + describe('getERC721TokenOwner', async () => { + it('should return the null address when tokenId is not owned', async () => { + const nonexistentTokenId = new BigNumber(1234567890); + expect( + await libAssetData.getERC721TokenOwner.callAsync(erc721TokenAddress, nonexistentTokenId), + ).to.be.equal(constants.NULL_ADDRESS); + }); + it('should return the owner address when tokenId is owned', async () => { + expect( + await libAssetData.getERC721TokenOwner.callAsync(erc721TokenAddress, firstERC721TokenId), + ).to.be.equal(tokenOwnerAddress); + }); + }); + + it('should query balance and allowance together, from asset data', async () => { + await setERC20AllowanceAsync(); + expect( + await libAssetData.getBalanceAndAllowance.callAsync( + tokenOwnerAddress, + approvedSpenderAddress, + await libAssetData.encodeERC20AssetData.callAsync(erc20TokenAddress), + ), + ).to.deep.equal([new BigNumber(erc20TokenTotalSupply), new BigNumber(1)]); + }); + + it('should query balances and allowances together, from an asset data array', async () => { + await setERC20AllowanceAsync(); + expect( + await libAssetData.getBatchBalancesAndAllowances.callAsync(tokenOwnerAddress, approvedSpenderAddress, [ + await libAssetData.encodeERC20AssetData.callAsync(erc20TokenAddress), + ]), + ).to.deep.equal([[new BigNumber(erc20TokenTotalSupply)], [new BigNumber(1)]]); + }); +}); diff --git a/contracts/asset-proxy/tsconfig.json b/contracts/asset-proxy/tsconfig.json index 31358217c9..809a8ed0da 100644 --- a/contracts/asset-proxy/tsconfig.json +++ b/contracts/asset-proxy/tsconfig.json @@ -9,6 +9,7 @@ "generated-artifacts/IAssetData.json", "generated-artifacts/IAssetProxy.json", "generated-artifacts/IAuthorizable.json", + "generated-artifacts/LibAssetData.json", "generated-artifacts/MixinAuthorizable.json", "generated-artifacts/MultiAssetProxy.json" ], diff --git a/contracts/coordinator/compiler.json b/contracts/coordinator/compiler.json index b5e9e3e156..1cc761b8f8 100644 --- a/contracts/coordinator/compiler.json +++ b/contracts/coordinator/compiler.json @@ -1,7 +1,7 @@ { "artifactsDir": "./generated-artifacts", "contractsDir": "./contracts", - "useDockerisedSolc": true, + "useDockerisedSolc": false, "compilerSettings": { "evmVersion": "constantinople", "optimizer": { diff --git a/contracts/erc20/compiler.json b/contracts/erc20/compiler.json index 576bff024b..f8a6170dce 100644 --- a/contracts/erc20/compiler.json +++ b/contracts/erc20/compiler.json @@ -1,7 +1,7 @@ { "artifactsDir": "./generated-artifacts", "contractsDir": "./contracts", - "useDockerisedSolc": true, + "useDockerisedSolc": false, "isOfflineMode": false, "compilerSettings": { "evmVersion": "constantinople", diff --git a/contracts/exchange-forwarder/compiler.json b/contracts/exchange-forwarder/compiler.json index d49cd34b7d..67f21df846 100644 --- a/contracts/exchange-forwarder/compiler.json +++ b/contracts/exchange-forwarder/compiler.json @@ -1,7 +1,7 @@ { "artifactsDir": "./generated-artifacts", "contractsDir": "./contracts", - "useDockerisedSolc": true, + "useDockerisedSolc": false, "isOfflineMode": false, "compilerSettings": { "evmVersion": "constantinople", diff --git a/contracts/extensions/compiler.json b/contracts/extensions/compiler.json index bf3756c15b..353533da19 100644 --- a/contracts/extensions/compiler.json +++ b/contracts/extensions/compiler.json @@ -1,7 +1,7 @@ { "artifactsDir": "./generated-artifacts", "contractsDir": "./contracts", - "useDockerisedSolc": true, + "useDockerisedSolc": false, "isOfflineMode": false, "compilerSettings": { "evmVersion": "constantinople", diff --git a/contracts/multisig/compiler.json b/contracts/multisig/compiler.json index 942df6bb25..b28414d97b 100644 --- a/contracts/multisig/compiler.json +++ b/contracts/multisig/compiler.json @@ -1,7 +1,7 @@ { "artifactsDir": "./generated-artifacts", "contractsDir": "./contracts", - "useDockerisedSolc": true, + "useDockerisedSolc": false, "isOfflineMode": false, "compilerSettings": { "evmVersion": "constantinople", diff --git a/contracts/utils/test/lib_address_array.ts b/contracts/utils/test/lib_address_array.ts index 0a3464a406..bb0d441a5c 100644 --- a/contracts/utils/test/lib_address_array.ts +++ b/contracts/utils/test/lib_address_array.ts @@ -23,11 +23,6 @@ describe('LibAddressArray', () => { before(async () => { await blockchainLifecycle.startAsync(); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - before(async () => { // Deploy LibAddressArray lib = await TestLibAddressArrayContract.deployFrom0xArtifactAsync( artifacts.TestLibAddressArray, @@ -35,10 +30,7 @@ describe('LibAddressArray', () => { txDefaults, ); }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { + after(async () => { await blockchainLifecycle.revertAsync(); });