diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index 59ab099079..40f67bad8e 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Do not reexport external dependencies", "pr": 1682 + }, + { + "note": "Add ERC1155Proxy", + "pr": 1661 } ] }, diff --git a/contracts/asset-proxy/compiler.json b/contracts/asset-proxy/compiler.json index 13d150d70c..28f73402ca 100644 --- a/contracts/asset-proxy/compiler.json +++ b/contracts/asset-proxy/compiler.json @@ -23,6 +23,7 @@ } }, "contracts": [ + "src/ERC1155Proxy.sol", "src/ERC20Proxy.sol", "src/ERC721Proxy.sol", "src/MixinAuthorizable.sol", diff --git a/contracts/asset-proxy/contracts/src/ERC1155Proxy.sol b/contracts/asset-proxy/contracts/src/ERC1155Proxy.sol new file mode 100644 index 0000000000..b11f0d8d7d --- /dev/null +++ b/contracts/asset-proxy/contracts/src/ERC1155Proxy.sol @@ -0,0 +1,267 @@ +/* + + 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 ids (*) | + // | | 68 | | 3. offset to values (*) | + // | | 100 | | 4. offset to data (*) | + // | Data | | | ids: | + // | | 132 | 32 | 1. ids Length | + // | | 164 | a | 2. ids Contents | + // | | | | values: | + // | | 164 + a | 32 | 1. values Length | + // | | 196 + a | b | 2. values Contents | + // | | | | data | + // | | 196 + a + b | 32 | 1. data Length | + // | | 228 + a + b | c | 2. data 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 ids (*) | + // | | 100 | | 4. offset to values (*) | + // | | 132 | | 5. offset to data (*) | + // | Data | | | ids: | + // | | 164 | 32 | 1. ids Length | + // | | 196 | a | 2. ids Contents | + // | | | | values: | + // | | 196 + a | 32 | 1. values Length | + // | | 228 + a | b | 2. values Contents | + // | | | | data | + // | | 228 + a + b | 32 | 1. data Length | + // | | 260 + a + b | c | 2. data 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 `values` (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 ids (*)` + // 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 `ids`, `values`, and `data` + + // 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, // aligned such that "offset to ids" is at the correct location for Table #3 + add(36, assetDataOffset), // beginning of asset data contents + assetDataLength // length of asset data + ) + + // Increment by 32 the offsets to `ids`, `values`, and `data` + 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 amount := calldataload(100) + let valuesOffset := add(mload(100), 4) // add 4 for calldata offset + let valuesLengthInBytes := mul( + mload(valuesOffset), + 32 + ) + let valuesBegin := add(valuesOffset, 32) + let valuesEnd := add(valuesBegin, valuesLengthInBytes) + for { let tokenValueOffset := valuesBegin } + lt(tokenValueOffset, valuesEnd) + { tokenValueOffset := add(tokenValueOffset, 32) } + { + // Load token value and generate scaled value + let tokenValue := mload(tokenValueOffset) + let scaledTokenValue := mul(tokenValue, amount) + + // Revert if `amount` != 0 and multiplication resulted in an overflow + if iszero(or( + iszero(amount), + eq(div(scaledTokenValue, amount), 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, // aligned such that `from` and `to` are at the correct location for Table #3 + 36, // beginning of `from` field from Table #1 + 64 // 32 bytes for `from` + 32 bytes for `to` field + ) + + ////////// 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..138485472a 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/@(ERC1155Proxy|ERC20Proxy|ERC721Proxy|IAssetData|IAssetProxy|IAuthorizable|MixinAuthorizable|MultiAssetProxy).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { @@ -70,6 +70,7 @@ "@0x/base-contract": "^5.0.2", "@0x/contracts-erc20": "^1.0.9", "@0x/contracts-erc721": "^1.0.9", + "@0x/contracts-erc1155": "^1.0.0", "@0x/contracts-utils": "2.0.1", "@0x/order-utils": "^7.0.2", "@0x/types": "^2.1.1", diff --git a/contracts/asset-proxy/src/artifacts.ts b/contracts/asset-proxy/src/artifacts.ts index 4cef199512..0dc768bc11 100644 --- a/contracts/asset-proxy/src/artifacts.ts +++ b/contracts/asset-proxy/src/artifacts.ts @@ -5,6 +5,7 @@ */ import { ContractArtifact } from 'ethereum-types'; +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'; @@ -15,6 +16,7 @@ import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; export const artifacts = { 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 06fa9dedc9..3412cb0266 100644 --- a/contracts/asset-proxy/src/wrappers.ts +++ b/contracts/asset-proxy/src/wrappers.ts @@ -3,6 +3,7 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ +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/erc1155_proxy.ts b/contracts/asset-proxy/test/erc1155_proxy.ts new file mode 100644 index 0000000000..7931fdfda0 --- /dev/null +++ b/contracts/asset-proxy/test/erc1155_proxy.ts @@ -0,0 +1,790 @@ +import { + artifacts as erc1155Artifacts, + DummyERC1155ReceiverBatchTokenReceivedEventArgs, + DummyERC1155ReceiverContract, + ERC1155MintableContract, + Erc1155Wrapper, +} from '@0x/contracts-erc1155'; +import { + chaiSetup, + constants, + expectTransactionFailedAsync, + expectTransactionFailedWithoutReasonAsync, + provider, + txDefaults, + web3Wrapper, +} from '@0x/contracts-test-utils'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { RevertReason } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { ERC1155ProxyWrapper, ERC721ProxyContract } from '../src'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +// tslint:disable:no-unnecessary-type-assertion +describe('ERC1155Proxy', () => { + // constant values used in transfer tests + const nftOwnerBalance = new BigNumber(1); + const nftNotOwnerBalance = new BigNumber(0); + const spenderInitialFungibleBalance = constants.INITIAL_ERC1155_FUNGIBLE_BALANCE; + const receiverInitialFungibleBalance = constants.INITIAL_ERC1155_FUNGIBLE_BALANCE; + const receiverContractInitialFungibleBalance = new BigNumber(0); + const fungibleValueToTransferSmall = spenderInitialFungibleBalance.div(100); + const fungibleValueToTransferLarge = spenderInitialFungibleBalance.div(4); + const valueMultiplierSmall = new BigNumber(2); + const valueMultiplierNft = new BigNumber(1); + const nonFungibleValueToTransfer = nftOwnerBalance; + const receiverCallbackData = '0x01020304'; + // addresses + let owner: string; + let notAuthorized: string; + let authorized: string; + let spender: string; + let receiver: string; + let receiverContract: string; + // contracts & wrappers + let erc1155Proxy: ERC721ProxyContract; + let erc1155Receiver: DummyERC1155ReceiverContract; + let erc1155ProxyWrapper: ERC1155ProxyWrapper; + let erc1155Contract: ERC1155MintableContract; + let erc1155Wrapper: Erc1155Wrapper; + // tokens + let fungibleTokens: BigNumber[]; + let nonFungibleTokensOwnedBySpender: BigNumber[]; + // tests + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + /// deploy & configure ERC1155Proxy + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, notAuthorized, authorized, spender, receiver] = _.slice(accounts, 0, 5)); + erc1155ProxyWrapper = new ERC1155ProxyWrapper(provider, usedAddresses, owner); + erc1155Proxy = await erc1155ProxyWrapper.deployProxyAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc1155Proxy.addAuthorizedAddress.sendTransactionAsync(authorized, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc1155Proxy.addAuthorizedAddress.sendTransactionAsync(erc1155Proxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // deploy & configure ERC1155 tokens and receiver + [erc1155Wrapper] = await erc1155ProxyWrapper.deployDummyContractsAsync(); + erc1155Contract = erc1155Wrapper.getContract(); + erc1155Receiver = await DummyERC1155ReceiverContract.deployFrom0xArtifactAsync( + erc1155Artifacts.DummyERC1155Receiver, + provider, + txDefaults, + ); + receiverContract = erc1155Receiver.address; + await erc1155ProxyWrapper.setBalancesAndAllowancesAsync(); + fungibleTokens = erc1155ProxyWrapper.getFungibleTokenIds(); + const nonFungibleTokens = erc1155ProxyWrapper.getNonFungibleTokenIds(); + const tokenBalances = await erc1155ProxyWrapper.getBalancesAsync(); + nonFungibleTokensOwnedBySpender = []; + _.each(nonFungibleTokens, (nonFungibleToken: BigNumber) => { + const nonFungibleTokenAsString = nonFungibleToken.toString(); + const nonFungibleTokenHeldBySpender = + tokenBalances.nonFungible[spender][erc1155Contract.address][nonFungibleTokenAsString][0]; + nonFungibleTokensOwnedBySpender.push(nonFungibleTokenHeldBySpender); + }); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('general', () => { + 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, fungible token', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = fungibleTokens.slice(0, 1); + const valuesToTransfer = [fungibleValueToTransferLarge]; + const valueMultiplier = valueMultiplierSmall; + // check balances before transfer + const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ); + // check balances after transfer + const totalValueTransferred = valuesToTransfer[0].times(valueMultiplier); + const expectedFinalBalances = [ + spenderInitialFungibleBalance.minus(totalValueTransferred), + receiverInitialFungibleBalance.plus(totalValueTransferred), + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances); + }); + it('should successfully transfer value for the same fungible token several times', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokenToTransfer = fungibleTokens[0]; + const tokensToTransfer = [tokenToTransfer, tokenToTransfer, tokenToTransfer]; + const valuesToTransfer = [ + fungibleValueToTransferSmall.plus(10), + fungibleValueToTransferSmall.plus(20), + fungibleValueToTransferSmall.plus(30), + ]; + const valueMultiplier = valueMultiplierSmall; + // check balances before transfer + const expectedInitialBalances = [ + // spender + spenderInitialFungibleBalance, + // receiver + receiverInitialFungibleBalance, + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, [tokenToTransfer], expectedInitialBalances); + // execute transfer + await erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ); + // check balances after transfer + let totalValueTransferred = _.reduce(valuesToTransfer, (sum: BigNumber, value: BigNumber) => { + return sum.plus(value); + }) as BigNumber; + totalValueTransferred = totalValueTransferred.times(valueMultiplier); + const expectedFinalBalances = [ + // spender + spenderInitialFungibleBalance.minus(totalValueTransferred), + // receiver + receiverInitialFungibleBalance.plus(totalValueTransferred), + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, [tokenToTransfer], expectedFinalBalances); + }); + it('should successfully transfer value for several fungible tokens', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = fungibleTokens.slice(0, 3); + const valuesToTransfer = [ + fungibleValueToTransferSmall.plus(10), + fungibleValueToTransferSmall.plus(20), + fungibleValueToTransferSmall.plus(30), + ]; + const valueMultiplier = valueMultiplierSmall; + // check balances before transfer + const expectedInitialBalances = [ + // spender + spenderInitialFungibleBalance, + spenderInitialFungibleBalance, + spenderInitialFungibleBalance, + // receiver + receiverInitialFungibleBalance, + receiverInitialFungibleBalance, + receiverInitialFungibleBalance, + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ); + // check balances after transfer + const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => { + return value.times(valueMultiplier); + }); + const expectedFinalBalances = [ + // spender + spenderInitialFungibleBalance.minus(totalValuesTransferred[0]), + spenderInitialFungibleBalance.minus(totalValuesTransferred[1]), + spenderInitialFungibleBalance.minus(totalValuesTransferred[2]), + // receiver + receiverInitialFungibleBalance.plus(totalValuesTransferred[0]), + receiverInitialFungibleBalance.plus(totalValuesTransferred[1]), + receiverInitialFungibleBalance.plus(totalValuesTransferred[2]), + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances); + }); + it('should successfully transfer a non-fungible token', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 1); + const valuesToTransfer = [nonFungibleValueToTransfer]; + const valueMultiplier = valueMultiplierNft; + // check balances before transfer + const expectedInitialBalances = [ + // spender + nftOwnerBalance, + // receiver + nftNotOwnerBalance, + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ); + // check balances after transfer + const expectedFinalBalances = [ + // spender + nftNotOwnerBalance, + // receiver + nftOwnerBalance, + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances); + }); + it('should successfully transfer multiple non-fungible tokens', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 3); + const valuesToTransfer = [ + nonFungibleValueToTransfer, + nonFungibleValueToTransfer, + nonFungibleValueToTransfer, + ]; + const valueMultiplier = valueMultiplierNft; + // check balances before transfer + const expectedInitialBalances = [ + // spender + nftOwnerBalance, + nftOwnerBalance, + nftOwnerBalance, + // receiver + nftNotOwnerBalance, + nftNotOwnerBalance, + nftNotOwnerBalance, + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ); + // check balances after transfer + const expectedFinalBalances = [ + // spender + nftNotOwnerBalance, + nftNotOwnerBalance, + nftNotOwnerBalance, + // receiver + nftOwnerBalance, + nftOwnerBalance, + nftOwnerBalance, + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances); + }); + it('should successfully transfer value for a combination of several fungible/non-fungible tokens', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const fungibleTokensToTransfer = fungibleTokens.slice(0, 3); + const nonFungibleTokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 2); + const tokensToTransfer = fungibleTokensToTransfer.concat(nonFungibleTokensToTransfer); + const valuesToTransfer = [ + fungibleValueToTransferLarge, + fungibleValueToTransferSmall, + fungibleValueToTransferSmall, + nonFungibleValueToTransfer, + nonFungibleValueToTransfer, + ]; + const valueMultiplier = valueMultiplierNft; + // check balances before transfer + const expectedInitialBalances = [ + // spender + spenderInitialFungibleBalance, + spenderInitialFungibleBalance, + spenderInitialFungibleBalance, + nftOwnerBalance, + nftOwnerBalance, + // receiver + receiverInitialFungibleBalance, + receiverInitialFungibleBalance, + receiverInitialFungibleBalance, + nftNotOwnerBalance, + nftNotOwnerBalance, + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ); + // check balances after transfer + const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => { + return value.times(valueMultiplier); + }); + const expectedFinalBalances = [ + // spender + expectedInitialBalances[0].minus(totalValuesTransferred[0]), + expectedInitialBalances[1].minus(totalValuesTransferred[1]), + expectedInitialBalances[2].minus(totalValuesTransferred[2]), + expectedInitialBalances[3].minus(totalValuesTransferred[3]), + expectedInitialBalances[4].minus(totalValuesTransferred[4]), + // receiver + expectedInitialBalances[5].plus(totalValuesTransferred[0]), + expectedInitialBalances[6].plus(totalValuesTransferred[1]), + expectedInitialBalances[7].plus(totalValuesTransferred[2]), + expectedInitialBalances[8].plus(totalValuesTransferred[3]), + expectedInitialBalances[9].plus(totalValuesTransferred[4]), + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances); + }); + it('should successfully transfer value to a smart contract and trigger its callback', async () => { + // setup test parameters + const tokenHolders = [spender, receiverContract]; + const tokensToTransfer = fungibleTokens.slice(0, 1); + const valuesToTransfer = [fungibleValueToTransferLarge]; + const valueMultiplier = valueMultiplierSmall; + const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => { + return value.times(valueMultiplier); + }); + // check balances before transfer + const expectedInitialBalances = [spenderInitialFungibleBalance, receiverContractInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + const txReceipt = await erc1155ProxyWrapper.transferFromWithLogsAsync( + spender, + receiverContract, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ); + // check receiver log ignored extra asset data + expect(txReceipt.logs.length).to.be.equal(2); + const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs< + DummyERC1155ReceiverBatchTokenReceivedEventArgs + >; + expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address); + expect(receiverLog.args.from).to.be.equal(spender); + expect(receiverLog.args.tokenIds.length).to.be.deep.equal(1); + expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokensToTransfer[0]); + expect(receiverLog.args.tokenValues.length).to.be.deep.equal(1); + expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(totalValuesTransferred[0]); + // note - if the `extraData` is ignored then the receiver log should ignore it as well. + expect(receiverLog.args.data).to.be.deep.equal(receiverCallbackData); + // check balances after transfer + const expectedFinalBalances = [ + expectedInitialBalances[0].minus(totalValuesTransferred[0]), + expectedInitialBalances[1].plus(totalValuesTransferred[0]), + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances); + }); + it('should successfully transfer value and ignore extra assetData', async () => { + // setup test parameters + const tokenHolders = [spender, receiverContract]; + const tokensToTransfer = fungibleTokens.slice(0, 1); + const valuesToTransfer = [fungibleValueToTransferLarge]; + const valueMultiplier = valueMultiplierSmall; + const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => { + return value.times(valueMultiplier); + }); + const extraData = '0102030405060708'; + // check balances before transfer + const expectedInitialBalances = [spenderInitialFungibleBalance, receiverContractInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + const txReceipt = await erc1155ProxyWrapper.transferFromWithLogsAsync( + spender, + receiverContract, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + extraData, + ); + // check receiver log ignored extra asset data + expect(txReceipt.logs.length).to.be.equal(2); + const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs< + DummyERC1155ReceiverBatchTokenReceivedEventArgs + >; + expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address); + expect(receiverLog.args.from).to.be.equal(spender); + expect(receiverLog.args.tokenIds.length).to.be.deep.equal(1); + expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokensToTransfer[0]); + expect(receiverLog.args.tokenValues.length).to.be.deep.equal(1); + expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(totalValuesTransferred[0]); + // note - if the `extraData` is ignored then the receiver log should ignore it as well. + expect(receiverLog.args.data).to.be.deep.equal(receiverCallbackData); + // check balances after transfer + const expectedFinalBalances = [ + expectedInitialBalances[0].minus(totalValuesTransferred[0]), + expectedInitialBalances[1].plus(totalValuesTransferred[0]), + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances); + }); + it('should transfer nothing if value is zero', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = fungibleTokens.slice(0, 1); + const valuesToTransfer = [new BigNumber(0)]; + const valueMultiplier = valueMultiplierSmall; + // check balances before transfer + const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ); + // check balances after transfer + const expectedFinalBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances); + }); + it('should transfer nothing if value multiplier is zero', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = fungibleTokens.slice(0, 1); + const valuesToTransfer = [fungibleValueToTransferLarge]; + const valueMultiplier = new BigNumber(0); + // check balances before transfer + const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ); + // check balances after transfer + const expectedFinalBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances); + }); + it('should transfer nothing if there are no tokens in asset data', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer: BigNumber[] = []; + const valuesToTransfer: BigNumber[] = []; + const valueMultiplier = valueMultiplierSmall; + // check balances before transfer + const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ); + // check balances after transfer + const expectedFinalBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances); + }); + it('should propagate revert reason from erc1155 contract failure', async () => { + // disable transfers + const shouldRejectTransfer = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await erc1155Receiver.setRejectTransferFlag.sendTransactionAsync(shouldRejectTransfer), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // setup test parameters + const tokenHolders = [spender, receiverContract]; + const tokensToTransfer = fungibleTokens.slice(0, 1); + const valuesToTransfer = [fungibleValueToTransferLarge]; + const valueMultiplier = valueMultiplierSmall; + // check balances before transfer + const expectedInitialBalances = [spenderInitialFungibleBalance, receiverContractInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await expectTransactionFailedAsync( + erc1155ProxyWrapper.transferFromAsync( + spender, + receiverContract, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ), + RevertReason.TransferRejected, + ); + }); + it('should revert if transferring the same non-fungible token more than once', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const nftToTransfer = nonFungibleTokensOwnedBySpender[0]; + const tokensToTransfer = [nftToTransfer, nftToTransfer]; + const valuesToTransfer = [nonFungibleValueToTransfer, nonFungibleValueToTransfer]; + const valueMultiplier = valueMultiplierNft; + // check balances before transfer + const expectedInitialBalances = [ + // spender + nftOwnerBalance, + nftOwnerBalance, + // receiver + nftNotOwnerBalance, + nftNotOwnerBalance, + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await expectTransactionFailedAsync( + erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ), + RevertReason.NFTNotOwnedByFromAddress, + ); + }); + it('should revert if there is a multiplication overflow', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 3); + const maxUintValue = new BigNumber(2).pow(256).minus(1); + const valuesToTransfer = [nonFungibleValueToTransfer, maxUintValue, nonFungibleValueToTransfer]; + const valueMultiplier = new BigNumber(2); + // check balances before transfer + const expectedInitialBalances = [ + // spender + nftOwnerBalance, + nftOwnerBalance, + nftOwnerBalance, + // receiver + nftNotOwnerBalance, + nftNotOwnerBalance, + nftNotOwnerBalance, + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + // note - this will overflow because we are trying to transfer `maxUintValue * 2` of the 2nd token + await expectTransactionFailedAsync( + erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ), + RevertReason.Uint256Overflow, + ); + }); + it('should revert if transferring > 1 instances of a non-fungible token (valueMultiplier field >1)', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 1); + const valuesToTransfer = [nonFungibleValueToTransfer]; + const valueMultiplier = new BigNumber(2); + // check balances before transfer + const expectedInitialBalances = [ + // spender + nftOwnerBalance, + // receiver + nftNotOwnerBalance, + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await expectTransactionFailedAsync( + erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ), + RevertReason.AmountEqualToOneRequired, + ); + }); + it('should revert if transferring > 1 instances of a non-fungible token (`valuesToTransfer` field >1)', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 1); + const valuesToTransfer = [new BigNumber(2)]; + const valueMultiplier = valueMultiplierNft; + // check balances before transfer + const expectedInitialBalances = [ + // spender + nftOwnerBalance, + // receiver + nftNotOwnerBalance, + ]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await expectTransactionFailedAsync( + erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ), + RevertReason.AmountEqualToOneRequired, + ); + }); + it('should revert if sender balance is insufficient', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = fungibleTokens.slice(0, 1); + const valueGreaterThanSpenderBalance = spenderInitialFungibleBalance.plus(1); + const valuesToTransfer = [valueGreaterThanSpenderBalance]; + const valueMultiplier = valueMultiplierSmall; + // check balances before transfer + const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await expectTransactionFailedAsync( + erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ), + RevertReason.Uint256Underflow, + ); + }); + it('should revert if sender allowance is insufficient', async () => { + // dremove allowance for ERC1155 proxy + const wrapper = erc1155ProxyWrapper.getContractWrapper(erc1155Contract.address); + const isApproved = false; + await wrapper.setApprovalForAllAsync(spender, erc1155Proxy.address, isApproved); + const isApprovedActualValue = await wrapper.isApprovedForAllAsync(spender, erc1155Proxy.address); + expect(isApprovedActualValue).to.be.equal(isApproved); + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = fungibleTokens.slice(0, 1); + const valuesToTransfer = [fungibleValueToTransferLarge]; + const valueMultiplier = valueMultiplierSmall; + // check balances before transfer + const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await expectTransactionFailedAsync( + erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorized, + ), + RevertReason.InsufficientAllowance, + ); + }); + it('should revert if caller is not authorized', async () => { + // setup test parameters + const tokenHolders = [spender, receiver]; + const tokensToTransfer = fungibleTokens.slice(0, 1); + const valuesToTransfer = [fungibleValueToTransferLarge]; + const valueMultiplier = valueMultiplierSmall; + // check balances before transfer + const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance]; + await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances); + // execute transfer + await expectTransactionFailedAsync( + erc1155ProxyWrapper.transferFromAsync( + spender, + receiver, + erc1155Contract.address, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + notAuthorized, + ), + RevertReason.SenderNotAuthorized, + ); + }); + }); +}); +// tslint:enable:no-unnecessary-type-assertion +// tslint:disable:max-file-line-count diff --git a/contracts/asset-proxy/test/utils/erc1155_proxy_wrapper.ts b/contracts/asset-proxy/test/utils/erc1155_proxy_wrapper.ts new file mode 100644 index 0000000000..4a43a4711b --- /dev/null +++ b/contracts/asset-proxy/test/utils/erc1155_proxy_wrapper.ts @@ -0,0 +1,383 @@ +import { artifacts as erc1155Artifacts, ERC1155MintableContract, Erc1155Wrapper } from '@0x/contracts-erc1155'; +import { + constants, + ERC1155FungibleHoldingsByOwner, + ERC1155HoldingsByOwner, + ERC1155NonFungibleHoldingsByOwner, + LogDecoder, + txDefaults, +} from '@0x/contracts-test-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { artifacts, ERC1155ProxyContract, IAssetProxyContract } from '../../src'; + +export class ERC1155ProxyWrapper { + private readonly _tokenOwnerAddresses: string[]; + private readonly _fungibleTokenIds: string[]; + private readonly _nonFungibleTokenIds: string[]; + private readonly _nfts: Array<{ id: BigNumber; tokenId: BigNumber }>; + private readonly _contractOwnerAddress: string; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _provider: Provider; + private readonly _logDecoder: LogDecoder; + private readonly _dummyTokenWrappers: Erc1155Wrapper[]; + private readonly _assetProxyInterface: IAssetProxyContract; + 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; + const allArtifacts = _.merge(artifacts, erc1155Artifacts); + this._logDecoder = new LogDecoder(this._web3Wrapper, allArtifacts); + this._dummyTokenWrappers = []; + this._assetProxyInterface = new IAssetProxyContract( + artifacts.IAssetProxy.compilerOutput.abi, + constants.NULL_ADDRESS, + provider, + ); + this._tokenOwnerAddresses = tokenOwnerAddresses; + this._contractOwnerAddress = contractOwnerAddress; + this._fungibleTokenIds = []; + this._nonFungibleTokenIds = []; + this._nfts = []; + } + /** + * @dev Deploys dummy ERC1155 contracts + * @return An array of ERC1155 wrappers; one for each deployed contract. + */ + public async deployDummyContractsAsync(): Promise { + // tslint:disable-next-line:no-unused-variable + for (const i of _.times(constants.NUM_DUMMY_ERC1155_CONTRACTS_TO_DEPLOY)) { + const erc1155Contract = await ERC1155MintableContract.deployFrom0xArtifactAsync( + erc1155Artifacts.ERC1155Mintable, + this._provider, + txDefaults, + ); + const erc1155Wrapper = new Erc1155Wrapper(erc1155Contract, this._provider, this._contractOwnerAddress); + this._dummyTokenWrappers.push(erc1155Wrapper); + } + return this._dummyTokenWrappers; + } + /** + * @dev Deploys the ERC1155 proxy + * @return Deployed ERC1155 proxy contract instance + */ + public async deployProxyAsync(): Promise { + this._proxyContract = await ERC1155ProxyContract.deployFrom0xArtifactAsync( + artifacts.ERC1155Proxy, + this._provider, + txDefaults, + ); + this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync(); + return this._proxyContract; + } + /** + * @dev Gets the ERC1155 proxy id + */ + public getProxyId(): string { + this._validateProxyContractExistsOrThrow(); + return this._proxyIdIfExists as string; + } + /** + * @dev transfers erc1155 fungible/non-fungible tokens. + * @param from source address + * @param to destination address + * @param contractAddress address of erc155 contract + * @param tokensToTransfer array of erc1155 tokens to transfer + * @param valuesToTransfer array of corresponding values for each erc1155 token to transfer + * @param valueMultiplier each value in `valuesToTransfer` is multiplied by this + * @param receiverCallbackData callback data if `to` is a contract + * @param authorizedSender sender of `transferFrom` transaction + * @param extraData extra data to append to `transferFrom` transaction. Optional. + * @return tranasction hash. + */ + public async transferFromAsync( + from: string, + to: string, + contractAddress: string, + tokensToTransfer: BigNumber[], + valuesToTransfer: BigNumber[], + valueMultiplier: BigNumber, + receiverCallbackData: string, + authorizedSender: string, + extraData?: string, + ): Promise { + this._validateProxyContractExistsOrThrow(); + let encodedAssetData = assetDataUtils.encodeERC1155AssetData( + contractAddress, + tokensToTransfer, + valuesToTransfer, + receiverCallbackData, + ); + if (!_.isUndefined(extraData)) { + encodedAssetData = `${encodedAssetData}${extraData}`; + } + const data = this._assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + from, + to, + valueMultiplier, + ); + const txHash = await this._web3Wrapper.sendTransactionAsync({ + to: (this._proxyContract as ERC1155ProxyContract).address, + data, + from: authorizedSender, + }); + return txHash; + } + /** + * @dev transfers erc1155 fungible/non-fungible tokens. + * @param from source address + * @param to destination address + * @param contractAddress address of erc155 contract + * @param tokensToTransfer array of erc1155 tokens to transfer + * @param valuesToTransfer array of corresponding values for each erc1155 token to transfer + * @param valueMultiplier each value in `valuesToTransfer` is multiplied by this + * @param receiverCallbackData callback data if `to` is a contract + * @param authorizedSender sender of `transferFrom` transaction + * @param extraData extra data to append to `transferFrom` transaction. Optional. + * @return tranasction receipt with decoded logs. + */ + public async transferFromWithLogsAsync( + from: string, + to: string, + contractAddress: string, + tokensToTransfer: BigNumber[], + valuesToTransfer: BigNumber[], + valueMultiplier: BigNumber, + receiverCallbackData: string, + authorizedSender: string, + extraData?: string, + ): Promise { + const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync( + await this.transferFromAsync( + from, + to, + contractAddress, + tokensToTransfer, + valuesToTransfer, + valueMultiplier, + receiverCallbackData, + authorizedSender, + extraData, + ), + ); + return txReceipt; + } + /** + * @dev For each deployed ERC1155 contract, this function mints a set of fungible/non-fungible + * tokens for each token owner address (`_tokenOwnerAddresses`). + * @return Balances of each token owner, across all ERC1155 contracts and tokens. + */ + public async setBalancesAndAllowancesAsync(): Promise { + this._validateDummyTokenContractsExistOrThrow(); + this._validateProxyContractExistsOrThrow(); + this._initialTokenIdsByOwner = { + fungible: {}, + nonFungible: {}, + }; + const fungibleHoldingsByOwner: ERC1155FungibleHoldingsByOwner = {}; + const nonFungibleHoldingsByOwner: ERC1155NonFungibleHoldingsByOwner = {}; + // Set balances accordingly + for (const dummyWrapper of this._dummyTokenWrappers) { + const dummyAddress = dummyWrapper.getContract().address; + // tslint:disable-next-line:no-unused-variable + for (const i of _.times(constants.NUM_ERC1155_FUNGIBLE_TOKENS_MINT)) { + // Create a fungible token + const tokenId = await dummyWrapper.mintFungibleTokensAsync( + this._tokenOwnerAddresses, + constants.INITIAL_ERC1155_FUNGIBLE_BALANCE, + ); + 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 + if (_.isUndefined(fungibleHoldingsByOwner[tokenOwnerAddress])) { + fungibleHoldingsByOwner[tokenOwnerAddress] = {}; + } + if (_.isUndefined(fungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress])) { + fungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress] = {}; + } + fungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress][tokenIdAsString] = + constants.INITIAL_ERC1155_FUNGIBLE_BALANCE; + await dummyWrapper.setApprovalForAllAsync( + tokenOwnerAddress, + (this._proxyContract as ERC1155ProxyContract).address, + true, + ); + } + } + // Non-fungible tokens + // tslint:disable-next-line:no-unused-variable + for (const j of _.times(constants.NUM_ERC1155_NONFUNGIBLE_TOKENS_MINT)) { + const [tokenId, nftIds] = await dummyWrapper.mintNonFungibleTokensAsync(this._tokenOwnerAddresses); + const tokenIdAsString = tokenId.toString(); + this._nonFungibleTokenIds.push(tokenIdAsString); + _.each(this._tokenOwnerAddresses, async (tokenOwnerAddress: string, i: number) => { + if (_.isUndefined(nonFungibleHoldingsByOwner[tokenOwnerAddress])) { + nonFungibleHoldingsByOwner[tokenOwnerAddress] = {}; + } + if (_.isUndefined(nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress])) { + nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress] = {}; + } + if (_.isUndefined(nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress][tokenIdAsString])) { + nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress][tokenIdAsString] = []; + } + this._nfts.push({ id: nftIds[i], tokenId }); + nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress][tokenIdAsString].push(nftIds[i]); + await dummyWrapper.setApprovalForAllAsync( + tokenOwnerAddress, + (this._proxyContract as ERC1155ProxyContract).address, + true, + ); + }); + } + } + this._initialTokenIdsByOwner = { + fungible: fungibleHoldingsByOwner, + nonFungible: nonFungibleHoldingsByOwner, + }; + return this._initialTokenIdsByOwner; + } + /** + * @dev For each deployed ERC1155 contract, this function quieries the set of fungible/non-fungible + * tokens for each token owner address (`_tokenOwnerAddresses`). + * @return Balances of each token owner, across all ERC1155 contracts and tokens. + */ + public async getBalancesAsync(): Promise { + this._validateDummyTokenContractsExistOrThrow(); + this._validateBalancesAndAllowancesSetOrThrow(); + const tokenHoldingsByOwner: ERC1155FungibleHoldingsByOwner = {}; + const nonFungibleHoldingsByOwner: ERC1155NonFungibleHoldingsByOwner = {}; + for (const dummyTokenWrapper of this._dummyTokenWrappers) { + const tokenContract = dummyTokenWrapper.getContract(); + const tokenAddress = tokenContract.address; + // 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 dummyTokenWrapper.getBalancesAsync(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; + } + /** + * @dev Checks if proxy is approved to transfer tokens on behalf of `userAddress`. + * @param userAddress owner of ERC1155 tokens. + * @param contractAddress address of ERC1155 contract. + * @return True iff the proxy is approved for all. False otherwise. + */ + public async isProxyApprovedForAllAsync(userAddress: string, contractAddress: string): Promise { + this._validateProxyContractExistsOrThrow(); + const tokenContract = this._getContractFromAddress(contractAddress); + const operator = (this._proxyContract as ERC1155ProxyContract).address; + const didApproveAll = await tokenContract.isApprovedForAll.callAsync(userAddress, operator); + return didApproveAll; + } + 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 getContractWrapper(contractAddress: string): Erc1155Wrapper { + const tokenWrapper = _.find(this._dummyTokenWrappers, (wrapper: Erc1155Wrapper) => { + return wrapper.getContract().address === contractAddress; + }); + if (_.isUndefined(tokenWrapper)) { + throw new Error(`Contract: ${contractAddress} was not deployed through ERC1155ProxyWrapper`); + } + return tokenWrapper; + } + private _getContractFromAddress(tokenAddress: string): ERC1155MintableContract { + const tokenContractIfExists = _.find(this._dummyTokenWrappers, c => c.getContract().address === tokenAddress); + if (_.isUndefined(tokenContractIfExists)) { + throw new Error(`Token: ${tokenAddress} was not deployed through ERC1155ProxyWrapper`); + } + return tokenContractIfExists.getContract(); + } + private _validateDummyTokenContractsExistOrThrow(): void { + if (_.isUndefined(this._dummyTokenWrappers)) { + 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..897806e703 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_proxy_wrapper'; diff --git a/contracts/asset-proxy/tsconfig.json b/contracts/asset-proxy/tsconfig.json index aaff338de9..31358217c9 100644 --- a/contracts/asset-proxy/tsconfig.json +++ b/contracts/asset-proxy/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "files": [ + "generated-artifacts/ERC1155Proxy.json", "generated-artifacts/ERC20Proxy.json", "generated-artifacts/ERC721Proxy.json", "generated-artifacts/IAssetData.json", diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index 5a01a0d2b5..c9088fbaab 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "3.1.0", + "changes": [ + { + "note": "Added ERC1155Proxy test constants and interfaces", + "pr": 1661 + } + ] + }, { "version": "3.0.9", "changes": [ diff --git a/contracts/test-utils/src/constants.ts b/contracts/test-utils/src/constants.ts index 99378c1fec..af56912ce3 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_CONTRACTS_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..2b21a7323e 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/0x.js/src/index.ts b/packages/0x.js/src/index.ts index 0a30cd3d5c..6656decc7c 100644 --- a/packages/0x.js/src/index.ts +++ b/packages/0x.js/src/index.ts @@ -91,6 +91,8 @@ export { SingleAssetData, ERC20AssetData, ERC721AssetData, + ERC1155AssetData, + ERC1155AssetDataAbi, MultiAssetData, MultiAssetDataWithRecursiveDecoding, SignatureType, diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts index f4d5898f41..9a1579104f 100644 --- a/packages/contract-wrappers/src/index.ts +++ b/packages/contract-wrappers/src/index.ts @@ -64,6 +64,7 @@ export { AssetData, ERC20AssetData, ERC721AssetData, + ERC1155AssetData, SingleAssetData, MultiAssetData, MultiAssetDataWithRecursiveDecoding, diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index 5d20b98fa1..0b2b7791f7 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "7.1.0", + "changes": [ + { + "note": "Added encoding/decoding fdor ERC1155 asset data", + "pr": 1661 + } + ] + }, { "timestamp": 1551479279, "version": "7.0.2", diff --git a/packages/order-utils/src/asset_data_utils.ts b/packages/order-utils/src/asset_data_utils.ts index f314891e20..2acf57d5f6 100644 --- a/packages/order-utils/src/asset_data_utils.ts +++ b/packages/order-utils/src/asset_data_utils.ts @@ -1,5 +1,8 @@ import { AssetProxyId, + ERC1155AssetData, + ERC1155AssetDataAbi, + ERC1155AssetDataNoProxyId, ERC20AssetData, ERC721AssetData, MultiAssetData, @@ -73,6 +76,47 @@ export const assetDataUtils = { tokenId: (decodedAssetData as any).tokenId, }; }, + /** + * Encodes a set of ERC1155 assets into an assetData string, usable in the makerAssetData or + * takerAssetData fields of a 0x order. + * @param tokenAddress The token address of the ERC1155 contract + * @param tokenIds The Id's of the ERC1155 tokens to transfer + * @param tokenValues The values of each respective token Id to transfer + * @param callbackData The data forwarded to a receiver, if receiver is a contract. + * @return The hex encoded assetData string + */ + encodeERC1155AssetData( + tokenAddress: string, + tokenIds: BigNumber[], + tokenValues: BigNumber[], + callbackData: string, + ): string { + const abiEncoder = AbiEncoder.createMethod('ERC1155Token', ERC1155AssetDataAbi); + const args = [tokenAddress, tokenIds, tokenValues, callbackData]; + const assetData = abiEncoder.encode(args, encodingRules); + return assetData; + }, + /** + * Decodes an ERC1155 assetData hex string into it's corresponding ERC1155 components. + * @param assetData Hex encoded assetData string to decode + * @return An object containing the decoded tokenAddress, tokenIds, tokenValues, callbackData & assetProxyId + */ + decodeERC1155AssetData(assetData: string): ERC1155AssetData { + const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData); + if (assetProxyId !== AssetProxyId.ERC1155) { + throw new Error(`Invalid assetProxyId. Expected '${AssetProxyId.ERC1155}', got '${assetProxyId}'`); + } + const abiEncoder = AbiEncoder.createMethod('ERC1155Token', ERC1155AssetDataAbi); + // tslint:disable-next-line:no-unnecessary-type-assertion + const decodedAssetData = abiEncoder.decode(assetData, decodingRules) as ERC1155AssetDataNoProxyId; + return { + assetProxyId, + tokenAddress: decodedAssetData.tokenAddress, + tokenIds: decodedAssetData.tokenIds, + tokenValues: decodedAssetData.tokenValues, + callbackData: decodedAssetData.callbackData, + }; + }, /** * Encodes assetData for multiple AssetProxies into a single hex encoded assetData string, usable in the makerAssetData or * takerAssetData fields in a 0x order. @@ -174,6 +218,7 @@ export const assetDataUtils = { if ( assetProxyId !== AssetProxyId.ERC20 && assetProxyId !== AssetProxyId.ERC721 && + assetProxyId !== AssetProxyId.ERC1155 && assetProxyId !== AssetProxyId.MultiAsset ) { throw new Error(`Invalid assetProxyId: ${assetProxyId}`); @@ -194,6 +239,13 @@ export const assetDataUtils = { isERC721AssetData(decodedAssetData: SingleAssetData | MultiAssetData): decodedAssetData is ERC721AssetData { return decodedAssetData.assetProxyId === AssetProxyId.ERC721; }, + /** + * Checks if the decoded asset data is valid ERC1155 data + * @param decodedAssetData The decoded asset data to check + */ + isERC1155AssetData(decodedAssetData: SingleAssetData | MultiAssetData): decodedAssetData is ERC1155AssetData { + return decodedAssetData.assetProxyId === AssetProxyId.ERC1155; + }, /** * Checks if the decoded asset data is valid MultiAsset data * @param decodedAssetData The decoded asset data to check @@ -243,6 +295,14 @@ export const assetDataUtils = { ); } }, + /** + * Throws if the assetData is not ERC1155. + * @param assetData Hex encoded assetData string + */ + assertIsERC1155AssetData(assetData: string): void { + // If the asset data is correctly decoded then it is valid. + assetDataUtils.decodeERC1155AssetData(assetData); + }, /** * Throws if the length or assetProxyId are invalid for the MultiAssetProxy. * @param assetData Hex encoded assetData string @@ -277,6 +337,9 @@ export const assetDataUtils = { case AssetProxyId.ERC721: assetDataUtils.assertIsERC721AssetData(assetData); break; + case AssetProxyId.ERC1155: + assetDataUtils.assertIsERC1155AssetData(assetData); + break; case AssetProxyId.MultiAsset: assetDataUtils.assertIsMultiAssetData(assetData); break; @@ -287,7 +350,7 @@ export const assetDataUtils = { /** * Decode any assetData into it's corresponding assetData object * @param assetData Hex encoded assetData string to decode - * @return Either a ERC20 or ERC721 assetData object + * @return Either a ERC20, ERC721, ERC1155, or MultiAsset assetData object */ decodeAssetDataOrThrow(assetData: string): SingleAssetData | MultiAssetData { const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData); @@ -298,6 +361,9 @@ export const assetDataUtils = { case AssetProxyId.ERC721: const erc721AssetData = assetDataUtils.decodeERC721AssetData(assetData); return erc721AssetData; + case AssetProxyId.ERC1155: + const erc1155AssetData = assetDataUtils.decodeERC1155AssetData(assetData); + return erc1155AssetData; case AssetProxyId.MultiAsset: const multiAssetData = assetDataUtils.decodeMultiAssetData(assetData); return multiAssetData; diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 2dc6fc71ca..68b50cc53f 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -46,6 +46,8 @@ export { SingleAssetData, ERC20AssetData, ERC721AssetData, + ERC1155AssetData, + ERC1155AssetDataAbi, MultiAssetData, MultiAssetDataWithRecursiveDecoding, AssetProxyId, diff --git a/packages/order-utils/test/asset_data_utils_test.ts b/packages/order-utils/test/asset_data_utils_test.ts index c498c5a00f..2634e7f595 100644 --- a/packages/order-utils/test/asset_data_utils_test.ts +++ b/packages/order-utils/test/asset_data_utils_test.ts @@ -20,6 +20,15 @@ const KNOWN_ERC721_ENCODING = { 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: + '0x9645780d0000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000003e90000000000000000000000000000000000000000000000000000000000002711000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000007d10000000000000000000000000000000000000000000000000000000000004e210000000000000000000000000000000000000000000000000000000000000044025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000', +}; const KNOWN_MULTI_ASSET_ENCODING = { amounts: [new BigNumber(1), new BigNumber(1)], nestedAssetData: [KNOWN_ERC20_ENCODING.assetData, KNOWN_ERC721_ENCODING.assetData], @@ -50,6 +59,23 @@ describe('assetDataUtils', () => { expect(decodedAssetData.assetProxyId).to.equal(AssetProxyId.ERC721); expect(decodedAssetData.tokenId).to.be.bignumber.equal(KNOWN_ERC721_ENCODING.tokenId); }); + it('should encode ERC1155', () => { + const assetData = assetDataUtils.encodeERC1155AssetData( + KNOWN_ERC1155_ENCODING.tokenAddress, + KNOWN_ERC1155_ENCODING.tokenIds, + KNOWN_ERC1155_ENCODING.tokenValues, + KNOWN_ERC1155_ENCODING.callbackData, + ); + expect(assetData).to.equal(KNOWN_ERC1155_ENCODING.assetData); + }); + it('should decode ERC1155', () => { + const decodedAssetData = assetDataUtils.decodeERC1155AssetData(KNOWN_ERC1155_ENCODING.assetData); + expect(decodedAssetData.assetProxyId).to.be.equal(AssetProxyId.ERC1155); + expect(decodedAssetData.tokenAddress).to.be.equal(KNOWN_ERC1155_ENCODING.tokenAddress); + expect(decodedAssetData.tokenValues).to.be.deep.equal(KNOWN_ERC1155_ENCODING.tokenValues); + expect(decodedAssetData.tokenIds).to.be.deep.equal(KNOWN_ERC1155_ENCODING.tokenIds); + expect(decodedAssetData.callbackData).to.be.equal(KNOWN_ERC1155_ENCODING.callbackData); + }); it('should encode ERC20 and ERC721 multiAssetData', () => { const assetData = assetDataUtils.encodeMultiAssetData( KNOWN_MULTI_ASSET_ENCODING.amounts, diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json index 36c97647b0..392c7adf15 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -1,4 +1,17 @@ [ + { + "version": "2.2.2", + "changes": [ + { + "note": "Added ERC1155 revert reasons", + "pr": 1657 + }, + { + "note": "Added `ERC1155AssetData`, `ERC1155AssetDataNoProxyId`, and `ERC1155AssetDataAbi`", + "pr": 1661 + } + ] + }, { "version": "2.2.1", "changes": [ diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ff2c87b79d..23cc118b2e 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,29 @@ export interface ERC721AssetData { tokenId: BigNumber; } -export type SingleAssetData = ERC20AssetData | ERC721AssetData; +export interface ERC1155AssetData { + assetProxyId: string; + tokenAddress: string; + tokenIds: BigNumber[]; + tokenValues: BigNumber[]; + callbackData: string; +} + +export interface ERC1155AssetDataNoProxyId { + tokenAddress: string; + tokenValues: BigNumber[]; + tokenIds: BigNumber[]; + callbackData: string; +} + +export const ERC1155AssetDataAbi = [ + { name: 'tokenAddress', type: 'address' }, + { name: 'tokenIds', type: 'uint256[]' }, + { name: 'tokenValues', type: 'uint256[]' }, + { name: 'callbackData', type: 'bytes' }, +]; + +export type SingleAssetData = ERC20AssetData | ERC721AssetData | ERC1155AssetData; export interface MultiAssetData { assetProxyId: string; diff --git a/yarn.lock b/yarn.lock index 4601cd467b..4f342df7da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11371,9 +11371,8 @@ nan@2.10.0, nan@>=2.5.1, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0, nan@^2.3.3, nan@^2. resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" nan@^2.11.0: - version "2.13.1" - resolved "https://registry.npmjs.org/nan/-/nan-2.13.1.tgz#a15bee3790bde247e8f38f1d446edcdaeb05f2dd" - integrity sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA== + version "2.12.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" nano-json-stream-parser@^0.1.2: version "0.1.2" @@ -13507,15 +13506,6 @@ react-dom@^16.3.2: object-assign "^4.1.1" prop-types "^15.6.0" -react-dom@^16.4.2: - version "16.8.4" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-16.8.4.tgz#1061a8e01a2b3b0c8160037441c3bf00a0e3bc48" - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.13.4" - react-dom@^16.5.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7" @@ -13573,6 +13563,8 @@ react-highlight@0xproject/react-highlight#react-peer-deps: dependencies: highlight.js "^9.11.0" highlightjs-solidity "^1.0.5" + react "^16.4.2" + react-dom "^16.4.2" react-hot-loader@^4.3.3: version "4.3.4" @@ -13817,15 +13809,6 @@ react@^16.3.2: object-assign "^4.1.1" prop-types "^15.6.0" -react@^16.4.2: - version "16.8.4" - resolved "https://registry.npmjs.org/react/-/react-16.8.4.tgz#fdf7bd9ae53f03a9c4cd1a371432c206be1c4768" - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.13.4" - react@^16.5.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42" @@ -14697,13 +14680,6 @@ schedule@^0.5.0: dependencies: object-assign "^4.1.1" -scheduler@^0.13.4: - version "0.13.4" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.13.4.tgz#8fef05e7a3580c76c0364d2df5e550e4c9140298" - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - schema-utils@^0.4.4: version "0.4.7" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" @@ -17678,8 +17654,7 @@ websocket@1.0.26: websocket@^1.0.26: version "1.0.28" - resolved "https://registry.npmjs.org/websocket/-/websocket-1.0.28.tgz#9e5f6fdc8a3fe01d4422647ef93abdd8d45a78d3" - integrity sha512-00y/20/80P7H4bCYkzuuvvfDvh+dgtXi5kzDf3UcZwN6boTYaKvsrtZ5lIYm1Gsg48siMErd9M4zjSYfYFHTrA== + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.28.tgz#9e5f6fdc8a3fe01d4422647ef93abdd8d45a78d3" dependencies: debug "^2.2.0" nan "^2.11.0"