diff --git a/contracts/asset-proxy/DEPLOYS.json b/contracts/asset-proxy/DEPLOYS.json index 0f25da1c3a..81b19daf59 100644 --- a/contracts/asset-proxy/DEPLOYS.json +++ b/contracts/asset-proxy/DEPLOYS.json @@ -1,4 +1,15 @@ [ + { + "name": "ERC1155Proxy", + "version": "1.0.0", + "changes": [ + { + "note": "Add ERC1155Proxy implementation", + "pr": 0, + "networks": { + } + } + ] { "name": "MultiAssetProxy", "version": "1.0.0", diff --git a/contracts/asset-proxy/contracts/src/ERC1155Proxy.sol b/contracts/asset-proxy/contracts/src/ERC1155Proxy.sol new file mode 100644 index 0000000000..410eaf4786 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/ERC1155Proxy.sol @@ -0,0 +1,257 @@ +/* + + 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 "./MixinAuthorizable.sol"; + + +contract ERC1155Proxy is + MixinAuthorizable +{ + + // Id of this proxy. + bytes4 constant internal PROXY_ID = bytes4(keccak256("ERC1155Token(address,uint256[],uint256[],bytes)")); + + function () + external + { + // Input calldata to this function is encoded as follows: + // -- TABLE #1 -- + // | 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 | (see below) | assetData Contents | + // + // + // Asset data is encoded as follows: + // -- TABLE #2 -- + // | Area | Offset | Length | Contents | + // |----------|-------------|---------|-------------------------------------| + // | Header | 0 | 4 | assetProxyId | + // | Params | | 4 * 32 | function parameters: | + // | | 4 | | 1. address of ERC1155 contract | + // | | 36 | | 2. offset to tokenIds (*) | + // | | 68 | | 3. offset to tokenValues (*) | + // | | 100 | | 4. offset to callbackData (*) | + // | Data | | | tokenIds: | + // | | 132 | 32 | 1. tokenIds Length | + // | | 164 | a | 2. tokenIds Contents | + // | | | | tokenValues: | + // | | 164 + a | 32 | 1. tokenValues Length | + // | | 196 + a | b | 2. tokenValues Contents | + // | | | | callbackData | + // | | 196 + a + b | 32 | 1. callbackData Length | + // | | 228 + a + b | c | 2. callbackData Contents | + // + // + // Calldata for target ERC155 asset is encoded for safeBatchTransferFrom: + // -- TABLE #3 -- + // | Area | Offset (**) | Length | Contents | + // |----------|-------------|---------|-------------------------------------| + // | Header | 0 | 4 | safeBatchTransferFrom selector | + // | Params | | 5 * 32 | function parameters: | + // | | 4 | | 1. from address | + // | | 36 | | 2. to address | + // | | 68 | | 3. offset to tokenIds (*) | + // | | 100 | | 4. offset to tokenValues (*) | + // | | 132 | | 5. offset to callbackData (*) | + // | Data | | | tokenIds: | + // | | 164 | 32 | 1. tokenIds Length | + // | | 196 | a | 2. tokenIds Contents | + // | | | | tokenValues: | + // | | 196 + a | 32 | 1. tokenValues Length | + // | | 228 + a | b | 2. tokenValues Contents | + // | | | | callbackData | + // | | 228 + a + b | 32 | 1. callbackData Length | + // | | 260 + a + b | c | 2. callbackData Contents | + // + // + // (*): offset is computed from start of function parameters, so offset + // by an additional 4 bytes in the calldata. + // + // (**): the `Offset` column is computed assuming no calldata compression; + // offsets in the Data Area are dynamic and should be evaluated in + // real-time. + // + // WARNING: The ABIv2 specification allows additional padding between + // the Params and Data section. This will result in a larger + // offset to assetData. + // + // Note: Table #1 and Table #2 exists in Calldata. We construct Table #3 in memory. + // + // + assembly { + // The first 4 bytes of calldata holds the function selector + let selector := and(calldataload(0), 0xffffffff00000000000000000000000000000000000000000000000000000000) + + // `transferFrom` will be called with the following parameters: + // assetData Encoded byte array. + // from Address to transfer asset from. + // to Address to transfer asset to. + // amount Amount of asset to transfer. + // bytes4(keccak256("transferFrom(bytes,address,address,uint256)")) = 0xa85e59e4 + if eq(selector, 0xa85e59e400000000000000000000000000000000000000000000000000000000) { + + // To lookup a value in a mapping, we load from the storage location keccak256(k, p), + // where k is the key left padded to 32 bytes and p is the storage slot + mstore(0, caller) + mstore(32, authorized_slot) + + // Revert if authorized[msg.sender] == false + if iszero(sload(keccak256(0, 64))) { + // Revert with `Error("SENDER_NOT_AUTHORIZED")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000001553454e4445525f4e4f545f415554484f52495a454400000000000000) + mstore(96, 0) + revert(0, 100) + } + + // Construct Table #3 in memory, starting at memory offset 0. + // The algorithm below maps asset data from Table #1 and Table #2 to Table #3, while + // scaling the `tokenValues` (Table #2) by `amount` (Table #1). Once Table #3 has + // been constructed in memory, the destination erc1155 contract is called using this + // as its calldata. This process is divided into four steps, below. + + ////////// STEP 1/4 ////////// + // Map relevant fields from assetData (Table #2) into memory (Table #3) + // The Contents column of Table #2 is the same as Table #3, + // beginning from parameter 3 - `offset to tokenIds (*)` + // The offsets in these rows are offset by 32 bytes in Table #3. + // Strategy: + // 1. Copy the assetData into memory at offset 32 + // 2. Increment by 32 the offsets to `tokenIds`, `tokenValues`, and `callbackData` + + // Load offset to `assetData` + let assetDataOffset := calldataload(4) + + // Load length in bytes of `assetData`, computed by: + // 4 (function selector) + // + assetDataOffset + let assetDataLength := calldataload(add(4, assetDataOffset)) + + // This corresponds to the beginning of the Data Area for Table #3. + // Computed by: + // 4 (function selector) + // + assetDataOffset + // + 32 (length of assetData) + calldatacopy(32, add(36, assetDataOffset), assetDataLength) + + // Increment by 32 the offsets to `tokenIds`, `tokenValues`, and `callbackData` + mstore(68, add(mload(68), 32)) + mstore(100, add(mload(100), 32)) + mstore(132, add(mload(132), 32)) + + // Record the address of the destination erc1155 asset for later. + let assetAddress := and( + mload(36), + 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff + ) + + ////////// STEP 2/4 ////////// + let scaleAmount := calldataload(100) + let tokenValuesOffset := add(mload(100), 4) // add 4 for calldata offset + let tokenValuesLengthInBytes := mul( + mload(tokenValuesOffset), + 32 + ) + let tokenValuesBegin := add(tokenValuesOffset, 32) + let tokenValuesEnd := add(tokenValuesBegin, add(tokenValuesLengthInBytes, 32)) + for { let tokenValueOffset := tokenValuesBegin } + lt(tokenValueOffset, tokenValuesEnd) + { tokenValueOffset := add(tokenValueOffset, 32) } + { + // Load token value and revert if multiplication would result in an overflow + let tokenValue := mload(tokenValueOffset) + let scaledTokenValue := mul(tokenValue, scaleAmount) + let expectedTokenValue := div(scaledTokenValue, scaleAmount) + + // check for multiplication overflow + if iszero(eq(expectedTokenValue, tokenValue)) { + // Revert with `Error("UINT256_OVERFLOW")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000001055494e543235365f4f564552464c4f57000000000000000000000000) + mstore(96, 0) + revert(0, 100) + } + + // There was no overflow, update `tokenValue` with its scaled counterpart + mstore(tokenValueOffset, scaledTokenValue) + } + + ////////// STEP 3/4 ////////// + // Store the safeBatchTransferFrom function selector, + // and copy `from`/`to` fields from Table #1 to Table #3. + + // The function selector is computed using: + // bytes4(keccak256("safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)")) + mstore(0, 0x2eb2c2d600000000000000000000000000000000000000000000000000000000) + + // Copy `from` and `to` fields from Table #1 to Table #3 + calldatacopy(4, 36, 64) + + ////////// STEP 4/4 ////////// + // Call into the destination erc1155 contract using as calldata Table #3 (constructed in-memory above) + let success := call( + gas, // forward all gas + assetAddress, // call address of erc1155 asset + 0, // don't send any ETH + 0, // pointer to start of input + add(assetDataLength, 32), // length of input (Table #3) is 32 bytes longer than `assetData` (Table #2) + 0, // write output over memory that won't be reused + 0 // don't copy output to memory + ) + + // Revert with reason given by AssetProxy if `transferFrom` call failed + if iszero(success) { + returndatacopy( + 0, // copy to memory at 0 + 0, // copy from return data at 0 + returndatasize() // copy all return data + ) + revert(0, returndatasize()) + } + + // Return if call was successful + return(0, 0) + } + + // Revert if undefined function is called + revert(0, 0) + } + } + + /// @dev Gets the proxy id associated with the proxy address. + /// @return Proxy id. + function getProxyId() + external + pure + returns (bytes4) + { + return PROXY_ID; + } +} diff --git a/contracts/asset-proxy/package.json b/contracts/asset-proxy/package.json index 2ab998dc74..3af5307d74 100644 --- a/contracts/asset-proxy/package.json +++ b/contracts/asset-proxy/package.json @@ -33,7 +33,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "./generated-artifacts/@(ERC20Proxy|ERC721Proxy|IAssetData|IAssetProxy|IAuthorizable|MixinAuthorizable|MultiAssetProxy).json", + "abis": "./generated-artifacts/@(ERC20Proxy|ERC721Proxy|ERC1155Proxy|IAssetData|IAssetProxy|IAuthorizable|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 4cef199512..c7c7f25dbb 100644 --- a/contracts/asset-proxy/src/artifacts.ts +++ b/contracts/asset-proxy/src/artifacts.ts @@ -5,6 +5,9 @@ */ import { ContractArtifact } from 'ethereum-types'; +import * as DummyERC1155Receiver from '../generated-artifacts/DummyERC1155Receiver.json'; +import * as ERC1155Mintable from '../generated-artifacts/ERC1155Mintable.json'; +import * as ERC1155Proxy from '../generated-artifacts/ERC1155Proxy.json'; import * as ERC20Proxy from '../generated-artifacts/ERC20Proxy.json'; import * as ERC721Proxy from '../generated-artifacts/ERC721Proxy.json'; import * as IAssetData from '../generated-artifacts/IAssetData.json'; @@ -13,6 +16,9 @@ import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json'; import * as MixinAuthorizable from '../generated-artifacts/MixinAuthorizable.json'; import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; export const artifacts = { + DummyERC1155Receiver: DummyERC1155Receiver as ContractArtifact, + ERC1155Mintable: ERC1155Mintable as ContractArtifact, + ERC1155Proxy: ERC1155Proxy as ContractArtifact, ERC20Proxy: ERC20Proxy as ContractArtifact, ERC721Proxy: ERC721Proxy as ContractArtifact, MixinAuthorizable: MixinAuthorizable as ContractArtifact, diff --git a/contracts/asset-proxy/src/wrappers.ts b/contracts/asset-proxy/src/wrappers.ts index 06fa9dedc9..50de349723 100644 --- a/contracts/asset-proxy/src/wrappers.ts +++ b/contracts/asset-proxy/src/wrappers.ts @@ -3,6 +3,9 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ +export * from '../generated-wrappers/dummy_erc1155_receiver'; +export * from '../generated-wrappers/erc1155_mintable'; +export * from '../generated-wrappers/erc1155_proxy'; export * from '../generated-wrappers/erc20_proxy'; export * from '../generated-wrappers/erc721_proxy'; export * from '../generated-wrappers/i_asset_data'; diff --git a/contracts/asset-proxy/test/proxies.ts b/contracts/asset-proxy/test/proxies.ts index 7977871358..744a42ab72 100644 --- a/contracts/asset-proxy/test/proxies.ts +++ b/contracts/asset-proxy/test/proxies.ts @@ -34,10 +34,16 @@ import { ERC20Wrapper, ERC721ProxyContract, ERC721Wrapper, + ERC1155Wrapper, + ERC1155ProxyContract, IAssetDataContract, IAssetProxyContract, MultiAssetProxyContract, + ERC1155MintableContract, + DummyERC1155ReceiverContract, + DummyERC1155ReceiverBatchTokenReceivedEventArgs, } from '../src'; +import values from 'ramda/es/values'; chaiSetup.configure(); const expect = chai.expect; @@ -68,15 +74,22 @@ describe('Asset Transfer Proxies', () => { let erc721Receiver: DummyERC721ReceiverContract; let erc20Proxy: ERC20ProxyContract; let erc721Proxy: ERC721ProxyContract; + let erc1155Proxy: ERC721ProxyContract; + let erc1155Receiver: DummyERC1155ReceiverContract; let noReturnErc20Token: DummyNoReturnERC20TokenContract; let multipleReturnErc20Token: DummyMultipleReturnERC20TokenContract; let multiAssetProxy: MultiAssetProxyContract; let erc20Wrapper: ERC20Wrapper; let erc721Wrapper: ERC721Wrapper; + let erc1155Wrapper: ERC1155Wrapper; let erc721AFromTokenId: BigNumber; let erc721BFromTokenId: BigNumber; + let erc1155Token: ERC1155MintableContract; + let erc1155FungibleTokenIds: BigNumber[]; + let erc1155NonFungibleTokenIds: BigNumber[]; + before(async () => { await blockchainLifecycle.startAsync(); }); @@ -89,10 +102,12 @@ describe('Asset Transfer Proxies', () => { erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + erc1155Wrapper = new ERC1155Wrapper(provider, usedAddresses, owner); // Deploy AssetProxies erc20Proxy = await erc20Wrapper.deployProxyAsync(); erc721Proxy = await erc721Wrapper.deployProxyAsync(); + erc1155Proxy = await erc1155Wrapper.deployProxyAsync(); multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync( artifacts.MultiAssetProxy, provider, @@ -127,6 +142,20 @@ describe('Asset Transfer Proxies', () => { constants.AWAIT_TRANSACTION_MINED_MS, ); + // Configure ERC1155Proxy + await web3Wrapper.awaitTransactionSuccessAsync( + await erc1155Proxy.addAuthorizedAddress.sendTransactionAsync(authorized, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc1155Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Configure MultiAssetProxy await web3Wrapper.awaitTransactionSuccessAsync( await multiAssetProxy.addAuthorizedAddress.sendTransactionAsync(authorized, { @@ -208,11 +237,21 @@ describe('Asset Transfer Proxies', () => { provider, txDefaults, ); - await erc721Wrapper.setBalancesAndAllowancesAsync(); const erc721Balances = await erc721Wrapper.getBalancesAsync(); erc721AFromTokenId = erc721Balances[fromAddress][erc721TokenA.address][0]; erc721BFromTokenId = erc721Balances[fromAddress][erc721TokenB.address][0]; + + // Deploy and configure ERC1155 tokens and receiver + [erc1155Token] = await erc1155Wrapper.deployDummyTokensAsync(); + erc1155Receiver = await DummyERC1155ReceiverContract.deployFrom0xArtifactAsync( + artifacts.DummyERC1155Receiver, + provider, + txDefaults, + ); + await erc1155Wrapper.setBalancesAndAllowancesAsync(); + erc1155FungibleTokenIds = erc1155Wrapper.getFungibleTokenIds(); + erc1155NonFungibleTokenIds = erc1155Wrapper.getNonFungibleTokenIds(); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); @@ -477,7 +516,6 @@ describe('Asset Transfer Proxies', () => { }); }); }); - describe('ERC721Proxy', () => { it('should revert if undefined function is called', async () => { const undefinedSelector = '0x01020304'; @@ -1286,6 +1324,373 @@ describe('Asset Transfer Proxies', () => { }); }); }); + describe.only('ERC1155Proxy', () => { + it('should revert if undefined function is called', async () => { + const undefinedSelector = '0x01020304'; + await expectTransactionFailedWithoutReasonAsync( + web3Wrapper.sendTransactionAsync({ + from: owner, + to: erc1155Proxy.address, + value: constants.ZERO_AMOUNT, + data: undefinedSelector, + }), + ); + }); + it('should have an id of 0x9645780d', async () => { + const proxyId = await erc1155Proxy.getProxyId.callAsync(); + // proxy computed using -- bytes4(keccak256("ERC1155Token(address,uint256[],uint256[],bytes)")); + const expectedProxyId = '0x9645780d'; + expect(proxyId).to.equal(expectedProxyId); + }); + describe('transferFrom', () => { + it('should successfully transfer value for a single token', async () => { + // Construct ERC1155 asset data + const callbackData = "0x"; + const fungibleTokenIdToTransfer = erc1155FungibleTokenIds[0]; + const tokenIdsToTransfer = [fungibleTokenIdToTransfer]; + const tokenValuesToTransfer = [new BigNumber(10)]; + const encodedAssetData = assetDataUtils.encodeERC1155AssetData(erc1155Token.address, tokenIdsToTransfer, tokenValuesToTransfer, callbackData); + // Verify pre-condition + const initialHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const initialSenderBalance = initialHoldingsByOwner.fungible[fromAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const initialReceiverBalance = initialHoldingsByOwner.fungible[toAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + // Perform a transfer from fromAddress to toAddress + const perUnitValue = new BigNumber(1000); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + perUnitValue, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc1155Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const totalValueTransferred = tokenValuesToTransfer[0].times(perUnitValue); + const newHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const newSenderBalance = newHoldingsByOwner.fungible[fromAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const expectedNewSenderBalance = initialSenderBalance.minus(totalValueTransferred); + const newReceiverBalance = newHoldingsByOwner.fungible[toAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const expectedNewReceiverBalance = initialReceiverBalance.plus(totalValueTransferred); + expect(newSenderBalance).to.be.bignumber.equal(expectedNewSenderBalance); + expect(newReceiverBalance).to.be.bignumber.equal(expectedNewReceiverBalance); + }); + it('should successfully transfer value for a collection of fungible tokens of the same id', async () => { + // Construct ERC1155 asset data + const callbackData = "0x"; + const fungibleTokenIdToTransfer = erc1155FungibleTokenIds[0]; + const tokenIdsToTransfer = [fungibleTokenIdToTransfer, fungibleTokenIdToTransfer, fungibleTokenIdToTransfer]; + const tokenValuesToTransfer = [new BigNumber(10), new BigNumber(20), new BigNumber(30)]; + const encodedAssetData = assetDataUtils.encodeERC1155AssetData(erc1155Token.address, tokenIdsToTransfer, tokenValuesToTransfer, callbackData); + // Verify pre-condition + const initialHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const initialSenderBalance = initialHoldingsByOwner.fungible[fromAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const initialReceiverBalance = initialHoldingsByOwner.fungible[toAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + // Perform a transfer from fromAddress to toAddress + const perUnitValue = new BigNumber(1000); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + perUnitValue, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc1155Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const totalValueTransferred = _.reduce(tokenValuesToTransfer, (sum: BigNumber, value: BigNumber) => {return sum.plus(value)}, new BigNumber(0)).times(perUnitValue); + const newHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const newSenderBalance = newHoldingsByOwner.fungible[fromAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const expectedNewSenderBalance = initialSenderBalance.minus(totalValueTransferred); + const newReceiverBalance = newHoldingsByOwner.fungible[toAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const expectedNewReceiverBalance = initialReceiverBalance.plus(totalValueTransferred); + expect(newSenderBalance).to.be.bignumber.equal(expectedNewSenderBalance); + expect(newReceiverBalance).to.be.bignumber.equal(expectedNewReceiverBalance); + }); + it('should successfully transfer value for a collection of fungible tokens of different ids', async () => { + // Construct ERC1155 asset data + const callbackData = "0x"; + const tokenIdsToTransfer = erc1155FungibleTokenIds.slice(0, 2); + const tokenValuesToTransfer = [new BigNumber(10), new BigNumber(20)]; + const encodedAssetData = assetDataUtils.encodeERC1155AssetData(erc1155Token.address, tokenIdsToTransfer, tokenValuesToTransfer, callbackData); + // Verify pre-condition + const initialHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const initialSenderBalances: BigNumber[] = []; + const initialReceiverBalances: BigNumber[] = []; + _.each(tokenIdsToTransfer, (tokenIdToTransfer: BigNumber) => { + initialSenderBalances.push(initialHoldingsByOwner.fungible[fromAddress][erc1155Token.address][tokenIdToTransfer.toString()]); + initialReceiverBalances.push(initialHoldingsByOwner.fungible[toAddress][erc1155Token.address][tokenIdToTransfer.toString()]); + }); + // Perform a transfer from fromAddress to toAddress + const perUnitValue = new BigNumber(1000); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + perUnitValue, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc1155Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + _.each(tokenIdsToTransfer, (tokenIdToTransfer: BigNumber, i: number) => { + const totalValueTransferred = tokenValuesToTransfer[i].times(perUnitValue); + const newSenderBalance = newHoldingsByOwner.fungible[fromAddress][erc1155Token.address][tokenIdToTransfer.toString()]; + const expectedNewSenderBalance = initialSenderBalances[i].minus(totalValueTransferred); + const newReceiverBalance = newHoldingsByOwner.fungible[toAddress][erc1155Token.address][tokenIdToTransfer.toString()]; + const expectedNewReceiverBalance = initialReceiverBalances[i].plus(totalValueTransferred); + expect(newSenderBalance).to.be.bignumber.equal(expectedNewSenderBalance); + expect(newReceiverBalance).to.be.bignumber.equal(expectedNewReceiverBalance); + }); + }); + it('should successfully transfer a non-fungible token', async () => { + // Construct ERC1155 asset data + const callbackData = "0x"; + const nonFungibleTokenIdToTransfer = erc1155NonFungibleTokenIds[0]; + const initialHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const nftToTransfer = initialHoldingsByOwner.nonFungible[fromAddress][erc1155Token.address][nonFungibleTokenIdToTransfer.toString()][0]; + const tokenIdsToTransfer = [nftToTransfer]; + const tokenValuesToTransfer = [new BigNumber(1)]; + const encodedAssetData = assetDataUtils.encodeERC1155AssetData(erc1155Token.address, tokenIdsToTransfer, tokenValuesToTransfer, callbackData); + // Verify precondition + const nftHolder = await erc1155Wrapper.ownerOfNonFungibleAsync(erc1155Token.address, nftToTransfer); + expect(nftHolder).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress + const perUnitValue = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + perUnitValue, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc1155Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newNftHolder = await erc1155Wrapper.ownerOfNonFungibleAsync(erc1155Token.address, nftToTransfer); + expect(newNftHolder).to.be.equal(toAddress); + // Verify balances updated successfully + const newHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const newNftsForFromAddress = newHoldingsByOwner.nonFungible[fromAddress][erc1155Token.address][nonFungibleTokenIdToTransfer.toString()]; + const newNftsForToAddress = newHoldingsByOwner.nonFungible[toAddress][erc1155Token.address][nonFungibleTokenIdToTransfer.toString()]; + expect(_.find(newNftsForFromAddress, nftToTransfer)).to.be.undefined(); + expect(_.find(newNftsForToAddress, nftToTransfer)).to.be.not.undefined(); + }); + it('should successfully transfer value for a combination of fungible/non-fungible tokens', async () => { + // Construct ERC1155 asset data + const callbackData = "0x"; + const fungibleTokenIdToTransfer = erc1155FungibleTokenIds[0]; + const nonFungibleTokenIdToTransfer = erc1155NonFungibleTokenIds[0]; + const initialHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const nftToTransfer = initialHoldingsByOwner.nonFungible[fromAddress][erc1155Token.address][nonFungibleTokenIdToTransfer.toString()][0]; + const tokenIdsToTransfer = [fungibleTokenIdToTransfer, nftToTransfer]; + const tokenValuesToTransfer = [new BigNumber(10), new BigNumber(1)]; + const encodedAssetData = assetDataUtils.encodeERC1155AssetData(erc1155Token.address, tokenIdsToTransfer, tokenValuesToTransfer, callbackData); + // Verify precondition + const initialSenderBalance = initialHoldingsByOwner.fungible[fromAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const initialReceiverBalance = initialHoldingsByOwner.fungible[toAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const nftHolder = await erc1155Wrapper.ownerOfNonFungibleAsync(erc1155Token.address, nftToTransfer); + expect(nftHolder).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress + const perUnitValue = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + perUnitValue, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc1155Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify non-fungible transfer was successful + const newNftHolder = await erc1155Wrapper.ownerOfNonFungibleAsync(erc1155Token.address, nftToTransfer); + expect(newNftHolder).to.be.equal(toAddress); + // Verify non-fungible balances updated successfully + const newHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const newNftsForFromAddress = newHoldingsByOwner.nonFungible[fromAddress][erc1155Token.address][nonFungibleTokenIdToTransfer.toString()]; + const newNftsForToAddress = newHoldingsByOwner.nonFungible[toAddress][erc1155Token.address][nonFungibleTokenIdToTransfer.toString()]; + expect(_.find(newNftsForFromAddress, nftToTransfer)).to.be.undefined(); + expect(_.find(newNftsForToAddress, nftToTransfer)).to.be.not.undefined(); + // Verify fungible transfer was successful + const totalValueTransferred = tokenValuesToTransfer[0].times(perUnitValue); + const newSenderBalance = newHoldingsByOwner.fungible[fromAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const expectedNewSenderBalance = initialSenderBalance.minus(totalValueTransferred); + const newReceiverBalance = newHoldingsByOwner.fungible[toAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const expectedNewReceiverBalance = initialReceiverBalance.plus(totalValueTransferred); + expect(newSenderBalance).to.be.bignumber.equal(expectedNewSenderBalance); + expect(newReceiverBalance).to.be.bignumber.equal(expectedNewReceiverBalance); + }); + it('should successfully transfer value and ignore extra assetData', async () => { + // Construct ERC1155 asset data + const callbackData = "0x"; + const fungibleTokenIdToTransfer = erc1155FungibleTokenIds[0]; + const tokenIdsToTransfer = [fungibleTokenIdToTransfer]; + const tokenValuesToTransfer = [new BigNumber(10)]; + const encodedAssetData = assetDataUtils.encodeERC1155AssetData(erc1155Token.address, tokenIdsToTransfer, tokenValuesToTransfer, callbackData); + const extraData = '0102030405060708'; + const encodedAssetDataPlusExtraData = `${encodedAssetData}${extraData}`; + // Verify pre-condition + const initialHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const initialSenderBalance = initialHoldingsByOwner.fungible[fromAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const initialReceiverBalance = initialHoldingsByOwner.fungible[toAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + // Perform a transfer from fromAddress to toAddress + const perUnitValue = new BigNumber(1000); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetDataPlusExtraData, + fromAddress, + toAddress, + perUnitValue, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc1155Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const totalValueTransferred = tokenValuesToTransfer[0].times(perUnitValue); + const newHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const newSenderBalance = newHoldingsByOwner.fungible[fromAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const expectedNewSenderBalance = initialSenderBalance.minus(totalValueTransferred); + const newReceiverBalance = newHoldingsByOwner.fungible[toAddress][erc1155Token.address][fungibleTokenIdToTransfer.toString()]; + const expectedNewReceiverBalance = initialReceiverBalance.plus(totalValueTransferred); + expect(newSenderBalance).to.be.bignumber.equal(expectedNewSenderBalance); + expect(newReceiverBalance).to.be.bignumber.equal(expectedNewReceiverBalance); + }); + it.only('should successfully execute callback when transferring to a smart contract', async () => { + // Construct ERC1155 asset data + const callbackData = "0x"; + const nonFungibleTokenIdToTransfer = erc1155NonFungibleTokenIds[0]; + const initialHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const nftToTransfer = initialHoldingsByOwner.nonFungible[fromAddress][erc1155Token.address][nonFungibleTokenIdToTransfer.toString()][0]; + const tokenIdsToTransfer = [nftToTransfer]; + const tokenValuesToTransfer = [new BigNumber(1)]; + const encodedAssetData = assetDataUtils.encodeERC1155AssetData(erc1155Token.address, tokenIdsToTransfer, tokenValuesToTransfer, callbackData); + // Verify precondition + const nftHolder = await erc1155Wrapper.ownerOfNonFungibleAsync(erc1155Token.address, nftToTransfer); + expect(nftHolder).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress + const perUnitValue = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + erc1155Receiver.address, + perUnitValue, + ); + const logDecoder = new LogDecoder(web3Wrapper, artifacts); + const txReceipt = await logDecoder.getTxWithDecodedLogsAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc1155Proxy.address, + data, + from: authorized, + }) + ); + // Verify logs + expect(txReceipt.logs.length).to.be.equal(2); + const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs; + expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address); + expect(receiverLog.args.from).to.be.equal(fromAddress); + expect(receiverLog.args.tokenIds.length).to.be.deep.equal(1); + expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokenIdsToTransfer[0]); + expect(receiverLog.args.tokenValues.length).to.be.deep.equal(1); + expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(tokenValuesToTransfer[0]); + expect(receiverLog.args.data).to.be.deep.equal(callbackData); + // Verify transfer was successful + const newNftHolder = await erc1155Wrapper.ownerOfNonFungibleAsync(erc1155Token.address, nftToTransfer); + expect(newNftHolder).to.be.equal(erc1155Receiver.address); + }); + it.only('should successfully execute callback when transferring to a smart conract when there is callback data', async () => { + // Construct ERC1155 asset data + const callbackData = "0x12345678"; + const nonFungibleTokenIdToTransfer = erc1155NonFungibleTokenIds[0]; + const initialHoldingsByOwner = await erc1155Wrapper.getBalancesAsync(); + const nftToTransfer = initialHoldingsByOwner.nonFungible[fromAddress][erc1155Token.address][nonFungibleTokenIdToTransfer.toString()][0]; + const tokenIdsToTransfer = [nftToTransfer]; + const tokenValuesToTransfer = [new BigNumber(1)]; + const encodedAssetData = assetDataUtils.encodeERC1155AssetData(erc1155Token.address, tokenIdsToTransfer, tokenValuesToTransfer, callbackData); + // Verify precondition + const nftHolder = await erc1155Wrapper.ownerOfNonFungibleAsync(erc1155Token.address, nftToTransfer); + expect(nftHolder).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress + const perUnitValue = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + erc1155Receiver.address, + perUnitValue, + ); + const logDecoder = new LogDecoder(web3Wrapper, artifacts); + const txReceipt = await logDecoder.getTxWithDecodedLogsAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc1155Proxy.address, + data, + from: authorized, + }) + ); + // Verify logs + expect(txReceipt.logs.length).to.be.equal(2); + const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs; + expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address); + expect(receiverLog.args.from).to.be.equal(fromAddress); + expect(receiverLog.args.tokenIds.length).to.be.deep.equal(1); + expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokenIdsToTransfer[0]); + expect(receiverLog.args.tokenValues.length).to.be.deep.equal(1); + expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(tokenValuesToTransfer[0]); + expect(receiverLog.args.data).to.be.deep.equal(callbackData); + // Verify transfer was successful + const newNftHolder = await erc1155Wrapper.ownerOfNonFungibleAsync(erc1155Token.address, nftToTransfer); + expect(newNftHolder).to.be.equal(erc1155Receiver.address); + }); + it('should propagate revert reason from erc1155 contract failure', async () => { + }); + it('should revert if transferring the same non-fungible token more than once', async () => { + }); + it('should revert if tansferring 0 amount of any token', async () => { + }); + it('should revert if there is a multiplication overflow', async () => { + }); + it('should revert if there is a multiplication overflow, when transferring multiple tokens', async () => { + }); + it('should revert if transferring > 1 instances of a non-fungible token (amount field >1)', async () => { + }); + it('should revert if transferring > 1 instances of a non-fungible token (value field >1)', async () => { + }); + it('should revert if sender balance is insufficient', async () => { + }); + it('should revert if sender allowance is insufficient', async () => { + }); + it('should revert if caller is not authorized', async () => { + }); + }); + }); }); // tslint:enable:no-unnecessary-type-assertion // tslint:disable:max-file-line-count diff --git a/contracts/asset-proxy/test/utils/erc1155_wrapper.ts b/contracts/asset-proxy/test/utils/erc1155_wrapper.ts new file mode 100644 index 0000000000..7f20ca25aa --- /dev/null +++ b/contracts/asset-proxy/test/utils/erc1155_wrapper.ts @@ -0,0 +1,288 @@ +import { constants, ERC1155HoldingsByOwner, ERC1155FungibleHoldingsByOwner, ERC1155NonFungibleHoldingsByOwner, LogDecoder, txDefaults } from '@0x/contracts-test-utils'; +import { generatePseudoRandomSalt } from '@0x/order-utils'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { LogWithDecodedArgs } from 'ethereum-types'; + +import { artifacts, ERC1155MintableContract, ERC1155ProxyContract, ERC1155MintableTransferSingleEventArgs } from '../../src'; + + +export class ERC1155Wrapper { + private readonly _tokenOwnerAddresses: string[]; + private readonly _fungibleTokenIds: string[]; + private readonly _nonFungibleTokenIds: string[]; + private readonly _nfts: {id: BigNumber, tokenId: BigNumber}[]; + private readonly _contractOwnerAddress: string; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _provider: Provider; + private readonly _logDecoder: LogDecoder; + private readonly _dummyTokenContracts: ERC1155MintableContract[]; + private _proxyContract?: ERC1155ProxyContract; + private _proxyIdIfExists?: string; + private _initialTokenIdsByOwner: ERC1155HoldingsByOwner = {fungible: {}, nonFungible: {}}; + constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) { + this._web3Wrapper = new Web3Wrapper(provider); + this._provider = provider; + this._logDecoder = new LogDecoder(this._web3Wrapper, artifacts); + this._dummyTokenContracts = []; + this._tokenOwnerAddresses = tokenOwnerAddresses; + this._contractOwnerAddress = contractOwnerAddress; + this._fungibleTokenIds = []; + this._nonFungibleTokenIds = []; + this._nfts = []; + } + public async deployDummyTokensAsync(): Promise { + // tslint:disable-next-line:no-unused-variable + for (const i of _.times(constants.NUM_DUMMY_ERC1155_TO_DEPLOY)) { + this._dummyTokenContracts.push( + await ERC1155MintableContract.deployFrom0xArtifactAsync( + artifacts.ERC1155Mintable, + this._provider, + txDefaults, + ), + ); + } + return this._dummyTokenContracts; + } + public async deployProxyAsync(): Promise { + this._proxyContract = await ERC1155ProxyContract.deployFrom0xArtifactAsync( + artifacts.ERC1155Proxy, + this._provider, + txDefaults, + ); + this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync(); + return this._proxyContract; + } + public getProxyId(): string { + this._validateProxyContractExistsOrThrow(); + return this._proxyIdIfExists as string; + } + public async setBalancesAndAllowancesAsync(): Promise { + this._validateDummyTokenContractsExistOrThrow(); + this._validateProxyContractExistsOrThrow(); + this._initialTokenIdsByOwner = { + fungible: {} as ERC1155FungibleHoldingsByOwner, + nonFungible: {} + }; + const fungibleHoldingsByOwner: ERC1155FungibleHoldingsByOwner = {}; + const nonFungibleHoldingsByOwner: ERC1155NonFungibleHoldingsByOwner = {}; + for (const dummyTokenContract of this._dummyTokenContracts) { + // Fungible Tokens + for (const i of _.times(constants.NUM_ERC1155_FUNGIBLE_TOKENS_MINT)) { + // Create a fungible token + const tokenUri = generatePseudoRandomSalt().toString(); + const tokenIsNonFungible = false; + const tokenId = await this.createTokenAsync(dummyTokenContract.address, tokenUri, tokenIsNonFungible); + const tokenIdAsString = tokenId.toString(); + this._fungibleTokenIds.push(tokenIdAsString); + // Mint tokens for each owner for this token + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + // tslint:disable-next-line:no-unused-variable + await this.mintFungibleAsync(dummyTokenContract.address, tokenId, tokenOwnerAddress); + if (_.isUndefined(fungibleHoldingsByOwner[tokenOwnerAddress])) { + fungibleHoldingsByOwner[tokenOwnerAddress] = {}; + } + if (_.isUndefined(fungibleHoldingsByOwner[tokenOwnerAddress][dummyTokenContract.address])) { + fungibleHoldingsByOwner[tokenOwnerAddress][dummyTokenContract.address] = {}; + } + fungibleHoldingsByOwner[tokenOwnerAddress][dummyTokenContract.address][tokenIdAsString] = constants.INITIAL_ERC1155_FUNGIBLE_BALANCE; + await this.approveProxyAsync(dummyTokenContract.address, tokenId, tokenOwnerAddress); + } + } + // Non-Fungible Tokens + for (const i of _.times(constants.NUM_ERC1155_NONFUNGIBLE_TOKENS_MINT)) { + const tokenUri = generatePseudoRandomSalt().toString(); + const tokenIsNonFungible = true; + const tokenId = await this.createTokenAsync(dummyTokenContract.address, tokenUri, tokenIsNonFungible); + const tokenIdAsString = tokenId.toString(); + this._nonFungibleTokenIds.push(tokenIdAsString); + await this.mintNonFungibleAsync(dummyTokenContract.address, tokenId, this._tokenOwnerAddresses); + let tokenNonce = 0; + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + if (_.isUndefined(nonFungibleHoldingsByOwner[tokenOwnerAddress])) { + nonFungibleHoldingsByOwner[tokenOwnerAddress] = {}; + } + if (_.isUndefined(nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyTokenContract.address])) { + nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyTokenContract.address] = {}; + } + if (_.isUndefined(nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyTokenContract.address][tokenIdAsString])) { + nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyTokenContract.address][tokenIdAsString] = []; + } + const nonFungibleId = tokenId.plus(++tokenNonce); + this._nfts.push({id: nonFungibleId, tokenId}); + nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyTokenContract.address][tokenIdAsString].push(nonFungibleId); + await this.approveProxyAsync(dummyTokenContract.address, tokenId, tokenOwnerAddress); + } + } + } + this._initialTokenIdsByOwner = { + fungible: fungibleHoldingsByOwner, + nonFungible: nonFungibleHoldingsByOwner, + } + return this._initialTokenIdsByOwner; + } + public async approveProxyAsync(tokenAddress: string, tokenId: BigNumber, tokenOwner: string): Promise { + const proxyAddress = (this._proxyContract as ERC1155ProxyContract).address; + await this.approveProxyForAllAsync(proxyAddress, tokenAddress, tokenOwner); + } + public async approveProxyForAllAsync(to: string, tokenAddress: string, tokenOwner: string): Promise { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.setApprovalForAll.sendTransactionAsync(to, true, { + from: tokenOwner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async createTokenAsync(tokenAddress: string, tokenUri: string, tokenIsNonFungible: boolean): Promise { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync( + await tokenContract.create.sendTransactionAsync(tokenUri, tokenIsNonFungible), + ); + const createFungibleTokenLog = txReceipt.logs[0] as LogWithDecodedArgs; + const dummyFungibleTokenId = createFungibleTokenLog.args._id; + return dummyFungibleTokenId; + } + public async mintFungibleAsync(tokenAddress: string, tokenId: BigNumber, userAddress: string): Promise { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.mintFungible.sendTransactionAsync( + tokenId, + [userAddress], + [constants.INITIAL_ERC1155_FUNGIBLE_BALANCE], + { from: this._contractOwnerAddress } + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async mintNonFungibleAsync(tokenAddress: string, tokenId: BigNumber, userAddresses: string[]): Promise { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + await this._web3Wrapper.awaitTransactionSuccessAsync( + await tokenContract.mintNonFungible.sendTransactionAsync( + tokenId, + userAddresses, + { from: this._contractOwnerAddress } + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + } + public async ownerOfNonFungibleAsync(tokenAddress: string, tokenId: BigNumber): Promise { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const owner = await tokenContract.ownerOf.callAsync(tokenId); + return owner; + } + public async isNonFungibleOwnerAsync(userAddress: string, tokenAddress: string, tokenId: BigNumber): Promise { + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const tokenOwner = await tokenContract.ownerOf.callAsync(tokenId); + const isOwner = tokenOwner === userAddress; + return isOwner; + } + public async isProxyApprovedForAllAsync(userAddress: string, tokenAddress: string): Promise { + this._validateProxyContractExistsOrThrow(); + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + const operator = (this._proxyContract as ERC1155ProxyContract).address; + const didApproveAll = await tokenContract.isApprovedForAll.callAsync(userAddress, operator); + return didApproveAll; + } + public async getBalancesAsync(): Promise { + this._validateDummyTokenContractsExistOrThrow(); + this._validateBalancesAndAllowancesSetOrThrow(); + const tokenHoldingsByOwner: ERC1155FungibleHoldingsByOwner = {}; + const nonFungibleHoldingsByOwner: ERC1155NonFungibleHoldingsByOwner = {}; + for (const dummyTokenContract of this._dummyTokenContracts) { + const tokenAddress = dummyTokenContract.address; + const tokenContract = this._getTokenContractFromAssetData(tokenAddress); + // Construct batch balance call + const tokenOwners: string[] = []; + const tokenIds: BigNumber[] = []; + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + for (const tokenId of this._fungibleTokenIds) { + tokenOwners.push(tokenOwnerAddress); + tokenIds.push(new BigNumber(tokenId)); + } + for (const nft of this._nfts) { + tokenOwners.push(tokenOwnerAddress); + tokenIds.push(nft.id); + } + } + const balances = await tokenContract.balanceOfBatch.callAsync(tokenOwners, tokenIds); + // Parse out balances into fungible / non-fungible token holdings + let i = 0; + for (const tokenOwnerAddress of this._tokenOwnerAddresses) { + // Fungible tokens + for (const tokenId of this._fungibleTokenIds) { + if (_.isUndefined(tokenHoldingsByOwner[tokenOwnerAddress])) { + tokenHoldingsByOwner[tokenOwnerAddress] = {}; + } + if (_.isUndefined(tokenHoldingsByOwner[tokenOwnerAddress][tokenAddress])) { + tokenHoldingsByOwner[tokenOwnerAddress][tokenAddress] = {}; + } + tokenHoldingsByOwner[tokenOwnerAddress][tokenAddress][tokenId] = balances[i++]; + } + // Non-fungible tokens + for (const nft of this._nfts) { + if (_.isUndefined(nonFungibleHoldingsByOwner[tokenOwnerAddress])) { + nonFungibleHoldingsByOwner[tokenOwnerAddress] = {}; + } + if (_.isUndefined(nonFungibleHoldingsByOwner[tokenOwnerAddress][tokenAddress])) { + nonFungibleHoldingsByOwner[tokenOwnerAddress][tokenAddress] = {}; + } + if (_.isUndefined(nonFungibleHoldingsByOwner[tokenOwnerAddress][tokenAddress][nft.tokenId.toString()])) { + nonFungibleHoldingsByOwner[tokenOwnerAddress][tokenAddress][nft.tokenId.toString()] = []; + } + const isOwner = balances[i++]; + if (isOwner.isEqualTo(1)) { + nonFungibleHoldingsByOwner[tokenOwnerAddress][tokenAddress][nft.tokenId.toString()].push(nft.id); + } + } + } + } + const holdingsByOwner = { + fungible: tokenHoldingsByOwner, + nonFungible: nonFungibleHoldingsByOwner, + } + return holdingsByOwner; + } + public getFungibleTokenIds(): BigNumber[] { + const fungibleTokenIds = _.map(this._fungibleTokenIds, (tokenIdAsString: string) => {return new BigNumber(tokenIdAsString)}); + return fungibleTokenIds; + } + public getNonFungibleTokenIds(): BigNumber[] { + const nonFungibleTokenIds = _.map(this._nonFungibleTokenIds, (tokenIdAsString: string) => {return new BigNumber(tokenIdAsString)}); + return nonFungibleTokenIds; + } + public getTokenOwnerAddresses(): string[] { + return this._tokenOwnerAddresses; + } + public getTokenAddresses(): string[] { + const tokenAddresses = _.map(this._dummyTokenContracts, dummyTokenContract => dummyTokenContract.address); + return tokenAddresses; + } + private _getTokenContractFromAssetData(tokenAddress: string): ERC1155MintableContract { + const tokenContractIfExists = _.find(this._dummyTokenContracts, c => c.address === tokenAddress); + if (_.isUndefined(tokenContractIfExists)) { + throw new Error(`Token: ${tokenAddress} was not deployed through ERC1155Wrapper`); + } + return tokenContractIfExists; + } + private _validateDummyTokenContractsExistOrThrow(): void { + if (_.isUndefined(this._dummyTokenContracts)) { + throw new Error('Dummy ERC1155 tokens not yet deployed, please call "deployDummyTokensAsync"'); + } + } + private _validateProxyContractExistsOrThrow(): void { + if (_.isUndefined(this._proxyContract)) { + throw new Error('ERC1155 proxy contract not yet deployed, please call "deployProxyAsync"'); + } + } + private _validateBalancesAndAllowancesSetOrThrow(): void { + if (_.keys(this._initialTokenIdsByOwner.fungible).length === 0 || _.keys(this._initialTokenIdsByOwner.nonFungible).length === 0) { + throw new Error( + 'Dummy ERC1155 balances and allowances not yet set, please call "setBalancesAndAllowancesAsync"', + ); + } + } +} diff --git a/contracts/asset-proxy/test/utils/index.ts b/contracts/asset-proxy/test/utils/index.ts index b11f6a45de..877ea5751d 100644 --- a/contracts/asset-proxy/test/utils/index.ts +++ b/contracts/asset-proxy/test/utils/index.ts @@ -1,2 +1,3 @@ export * from './erc20_wrapper'; export * from './erc721_wrapper'; +export * from './erc1155_wrapper'; diff --git a/contracts/asset-proxy/tsconfig.json b/contracts/asset-proxy/tsconfig.json index aaff338de9..2ea279b4e5 100644 --- a/contracts/asset-proxy/tsconfig.json +++ b/contracts/asset-proxy/tsconfig.json @@ -3,6 +3,9 @@ "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "files": [ + "generated-artifacts/DummyERC1155Receiver.json", + "generated-artifacts/ERC1155Mintable.json", + "generated-artifacts/ERC1155Proxy.json", "generated-artifacts/ERC20Proxy.json", "generated-artifacts/ERC721Proxy.json", "generated-artifacts/IAssetData.json", diff --git a/contracts/test-utils/src/constants.ts b/contracts/test-utils/src/constants.ts index 99378c1fec..e1d9b8dbb4 100644 --- a/contracts/test-utils/src/constants.ts +++ b/contracts/test-utils/src/constants.ts @@ -38,11 +38,16 @@ export const constants = { NUM_DUMMY_ERC20_TO_DEPLOY: 3, NUM_DUMMY_ERC721_TO_DEPLOY: 2, NUM_ERC721_TOKENS_TO_MINT: 2, + NUM_DUMMY_ERC1155_TO_DEPLOY: 1, + NUM_ERC1155_FUNGIBLE_TOKENS_MINT: 3, + NUM_ERC1155_NONFUNGIBLE_TOKENS_MINT: 3, NULL_ADDRESS: '0x0000000000000000000000000000000000000000', UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), TESTRPC_PRIVATE_KEYS: _.map(TESTRPC_PRIVATE_KEYS_STRINGS, privateKeyString => ethUtil.toBuffer(privateKeyString)), INITIAL_ERC20_BALANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18), INITIAL_ERC20_ALLOWANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18), + INITIAL_ERC1155_FUNGIBLE_BALANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18), + INITIAL_ERC1155_FUNGIBLE_ALLOWANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18), STATIC_ORDER_PARAMS: { makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18), takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18), diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index 331913af80..c12a71bfda 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -32,6 +32,9 @@ export { MarketBuyOrders, MarketSellOrders, ERC721TokenIdsByOwner, + ERC1155FungibleHoldingsByOwner, + ERC1155NonFungibleHoldingsByOwner, + ERC1155HoldingsByOwner, OrderStatus, AllowanceAmountScenario, AssetDataScenario, diff --git a/contracts/test-utils/src/types.ts b/contracts/test-utils/src/types.ts index 16c3a9f3da..9cd098bc6a 100644 --- a/contracts/test-utils/src/types.ts +++ b/contracts/test-utils/src/types.ts @@ -14,6 +14,27 @@ export interface ERC721TokenIdsByOwner { }; } +export interface ERC1155FungibleHoldingsByOwner { + [ownerAddress: string]: { + [tokenAddress: string]: { + [tokenId: string]: BigNumber + } + }; +} + +export interface ERC1155NonFungibleHoldingsByOwner { + [ownerAddress: string]: { + [tokenAddress: string]: { + [tokenId: string]: BigNumber[] + } + }; +} + +export interface ERC1155HoldingsByOwner { + fungible: ERC1155FungibleHoldingsByOwner, + nonFungible: ERC1155NonFungibleHoldingsByOwner, +} + export interface SubmissionContractEventArgs { transactionId: BigNumber; } diff --git a/packages/order-utils/src/asset_data_utils.ts b/packages/order-utils/src/asset_data_utils.ts index f314891e20..ab8aa3443e 100644 --- a/packages/order-utils/src/asset_data_utils.ts +++ b/packages/order-utils/src/asset_data_utils.ts @@ -94,6 +94,18 @@ export const assetDataUtils = { const assetData = abiEncoder.encode(args, encodingRules); return assetData; }, + /** + * Encodes assetData for multiple AssetProxies into a single hex encoded assetData string, usable in the makerAssetData or + * takerAssetData fields in a 0x order. + + * @return The hex encoded assetData string + */ + encodeERC1155AssetData(tokenAddress: string, tokenIds: BigNumber[], tokenValues: BigNumber[], callbackData: string): string { + const abiEncoder = AbiEncoder.createMethod('ERC1155Token', ['address','uint256[]','uint256[]','bytes']); + const args = [tokenAddress, tokenIds, tokenValues, callbackData]; + const assetData = abiEncoder.encode(args, encodingRules); + return assetData; + }, /** * Decodes a MultiAsset assetData hex string into it's corresponding amounts and nestedAssetData * @param assetData Hex encoded assetData string to decode @@ -174,6 +186,7 @@ export const assetDataUtils = { if ( assetProxyId !== AssetProxyId.ERC20 && assetProxyId !== AssetProxyId.ERC721 && + assetProxyId !== AssetProxyId.ERC1155 && assetProxyId !== AssetProxyId.MultiAsset ) { throw new Error(`Invalid assetProxyId: ${assetProxyId}`); @@ -201,6 +214,9 @@ export const assetDataUtils = { isMultiAssetData(decodedAssetData: SingleAssetData | MultiAssetData): decodedAssetData is MultiAssetData { return decodedAssetData.assetProxyId === AssetProxyId.MultiAsset; }, + isER1155AssetData(decodedAssetData: SingleAssetData | MultiAssetData): decodedAssetData is MultiAssetData { + return decodedAssetData.assetProxyId === AssetProxyId.MultiAsset; + }, /** * Throws if the length or assetProxyId are invalid for the ERC20Proxy. * @param assetData Hex encoded assetData string diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ff2c87b79d..89a7d35da5 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -163,6 +163,7 @@ export enum AssetProxyId { ERC20 = '0xf47261b0', ERC721 = '0x02571792', MultiAsset = '0x94cfcdd7', + ERC1155 = '0x9645780d', } export interface ERC20AssetData { @@ -176,7 +177,15 @@ export interface ERC721AssetData { tokenId: BigNumber; } -export type SingleAssetData = ERC20AssetData | ERC721AssetData; +export interface ERC1155AssetData { + assetProxyId: string; + tokenAddress: string; + tokenIds: BigNumber[]; + tokenValues: BigNumber[]; + callbackData: string; +} + +export type SingleAssetData = ERC20AssetData | ERC721AssetData | ERC1155AssetData; export interface MultiAssetData { assetProxyId: string;