diff --git a/contracts/erc721/src/index.ts b/contracts/erc721/src/index.ts index 47507e4420..707c0ca511 100644 --- a/contracts/erc721/src/index.ts +++ b/contracts/erc721/src/index.ts @@ -2,6 +2,8 @@ export { DummyERC721ReceiverContract, DummyERC721TokenContract, ERC721TokenContract, + ERC721TokenEvents, + ERC721TokenTransferEventArgs, IERC721ReceiverContract, } from './wrappers'; export { artifacts } from './artifacts'; diff --git a/contracts/exchange-forwarder/CHANGELOG.json b/contracts/exchange-forwarder/CHANGELOG.json index 1b2b054a07..1d4a0640c4 100644 --- a/contracts/exchange-forwarder/CHANGELOG.json +++ b/contracts/exchange-forwarder/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "3.1.0-beta.4", + "changes": [ + { + "note": "Added buy support for ERC20Bridge", + "pr": 2356 + } + ] + }, { "version": "3.1.0-beta.3", "changes": [ diff --git a/contracts/exchange-forwarder/contracts/src/Forwarder.sol b/contracts/exchange-forwarder/contracts/src/Forwarder.sol index 36380bc730..dd83b57368 100644 --- a/contracts/exchange-forwarder/contracts/src/Forwarder.sol +++ b/contracts/exchange-forwarder/contracts/src/Forwarder.sol @@ -32,13 +32,13 @@ contract Forwarder is { constructor ( address _exchange, - bytes memory _wethAssetData + address _weth ) public Ownable() LibConstants( _exchange, - _wethAssetData + _weth ) MixinForwarderCore() {} diff --git a/contracts/exchange-forwarder/contracts/src/MixinAssets.sol b/contracts/exchange-forwarder/contracts/src/MixinAssets.sol index 6813c45e98..eb0e5c1392 100644 --- a/contracts/exchange-forwarder/contracts/src/MixinAssets.sol +++ b/contracts/exchange-forwarder/contracts/src/MixinAssets.sol @@ -21,9 +21,9 @@ pragma solidity ^0.5.9; import "@0x/contracts-utils/contracts/src/LibBytes.sol"; import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/Ownable.sol"; -import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; import "@0x/contracts-erc721/contracts/src/interfaces/IERC721Token.sol"; +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol"; import "./libs/LibConstants.sol"; import "./libs/LibForwarderRichErrors.sol"; import "./interfaces/IAssets.sol"; @@ -36,13 +36,10 @@ contract MixinAssets is { using LibBytes for bytes; - bytes4 constant internal ERC20_TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)")); - - /// @dev Withdraws assets from this contract. The contract formerly required a ZRX balance in order - /// to function optimally, and this function allows the ZRX to be withdrawn by owner. - /// It may also be used to withdraw assets that were accidentally sent to this contract. + /// @dev Withdraws assets from this contract. It may be used by the owner to withdraw assets + /// that were accidentally sent to this contract. /// @param assetData Byte array encoded for the respective asset proxy. - /// @param amount Amount of ERC20 token to withdraw. + /// @param amount Amount of the asset to withdraw. function withdrawAsset( bytes calldata assetData, uint256 amount @@ -63,14 +60,16 @@ contract MixinAssets is external { bytes4 proxyId = assetData.readBytes4(0); + bytes4 erc20ProxyId = IAssetData(address(0)).ERC20Token.selector; + // For now we only care about ERC20, since percentage fees on ERC721 tokens are invalid. - if (proxyId == ERC20_DATA_ID) { - address proxyAddress = EXCHANGE.getAssetProxy(ERC20_DATA_ID); + if (proxyId == erc20ProxyId) { + address proxyAddress = EXCHANGE.getAssetProxy(erc20ProxyId); if (proxyAddress == address(0)) { LibRichErrors.rrevert(LibForwarderRichErrors.UnregisteredAssetProxyError()); } - IERC20Token assetToken = IERC20Token(assetData.readAddress(16)); - assetToken.approve(proxyAddress, MAX_UINT); + address token = assetData.readAddress(16); + LibERC20Token.approve(token, proxyAddress, MAX_UINT); } } @@ -85,9 +84,12 @@ contract MixinAssets is { bytes4 proxyId = assetData.readBytes4(0); - if (proxyId == ERC20_DATA_ID) { + if ( + proxyId == IAssetData(address(0)).ERC20Token.selector || + proxyId == IAssetData(address(0)).ERC20Bridge.selector + ) { _transferERC20Token(assetData, amount); - } else if (proxyId == ERC721_DATA_ID) { + } else if (proxyId == IAssetData(address(0)).ERC721Token.selector) { _transferERC721Token(assetData, amount); } else { LibRichErrors.rrevert(LibForwarderRichErrors.UnsupportedAssetProxyError( @@ -96,7 +98,7 @@ contract MixinAssets is } } - /// @dev Decodes ERC20 assetData and transfers given amount to sender. + /// @dev Decodes ERC20 or ERC20Bridge assetData and transfers given amount to sender. /// @param assetData Byte array encoded for the respective asset proxy. /// @param amount Amount of asset to transfer to sender. function _transferERC20Token( diff --git a/contracts/exchange-forwarder/contracts/src/MixinExchangeWrapper.sol b/contracts/exchange-forwarder/contracts/src/MixinExchangeWrapper.sol index 235716eecf..16782c5cc3 100644 --- a/contracts/exchange-forwarder/contracts/src/MixinExchangeWrapper.sol +++ b/contracts/exchange-forwarder/contracts/src/MixinExchangeWrapper.sol @@ -19,12 +19,15 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; +import "@0x/contracts-utils/contracts/src/LibBytes.sol"; import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibFillResults.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol"; +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol"; +import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; import "./libs/LibConstants.sol"; import "./libs/LibForwarderRichErrors.sol"; import "./MixinAssets.sol"; @@ -34,6 +37,7 @@ contract MixinExchangeWrapper is LibConstants, MixinAssets { + using LibBytes for bytes; using LibSafeMath for uint256; /// @dev Fills the input order. @@ -88,7 +92,10 @@ contract MixinExchangeWrapper is ) { // No taker fee or percentage fee - if (order.takerFee == 0 || order.takerFeeAssetData.equals(order.makerAssetData)) { + if ( + order.takerFee == 0 || + _areUnderlyingAssetsEqual(order.takerFeeAssetData, order.makerAssetData) + ) { // Attempt to sell the remaining amount of WETH LibFillResults.FillResults memory singleFillResults = _fillOrderNoThrow( order, @@ -103,7 +110,7 @@ contract MixinExchangeWrapper is makerAssetAcquiredAmount = singleFillResults.makerAssetFilledAmount .safeSub(singleFillResults.takerFeePaid); // WETH fee - } else if (order.takerFeeAssetData.equals(order.takerAssetData)) { + } else if (_areUnderlyingAssetsEqual(order.takerFeeAssetData, order.takerAssetData)) { // We will first sell WETH as the takerAsset, then use it to pay the takerFee. // This ensures that we reserve enough to pay the taker and protocol fees. @@ -150,9 +157,10 @@ contract MixinExchangeWrapper is uint256 totalMakerAssetAcquiredAmount ) { - uint256 ordersLength = orders.length; uint256 protocolFee = tx.gasprice.safeMul(EXCHANGE.protocolFeeMultiplier()); + bytes4 erc20BridgeProxyId = IAssetData(address(0)).ERC20Bridge.selector; + uint256 ordersLength = orders.length; for (uint256 i = 0; i != ordersLength; i++) { // Preemptively skip to avoid division by zero in _marketSellSingleOrder if (orders[i].makerAssetAmount == 0 || orders[i].takerAssetAmount == 0) { @@ -164,6 +172,15 @@ contract MixinExchangeWrapper is .safeSub(totalWethSpentAmount) .safeSub(protocolFee); + // If the maker asset is ERC20Bridge, take a snapshot of the Forwarder contract's balance. + bytes4 makerAssetProxyId = orders[i].makerAssetData.readBytes4(0); + address tokenAddress; + uint256 balanceBefore; + if (makerAssetProxyId == erc20BridgeProxyId) { + tokenAddress = orders[i].makerAssetData.readAddress(16); + balanceBefore = IERC20Token(tokenAddress).balanceOf(address(this)); + } + ( uint256 wethSpentAmount, uint256 makerAssetAcquiredAmount @@ -173,6 +190,15 @@ contract MixinExchangeWrapper is remainingTakerAssetFillAmount ); + // Account for the ERC20Bridge transfering more of the maker asset than expected. + if (makerAssetProxyId == erc20BridgeProxyId) { + uint256 balanceAfter = IERC20Token(tokenAddress).balanceOf(address(this)); + makerAssetAcquiredAmount = LibSafeMath.max256( + balanceAfter.safeSub(balanceBefore), + makerAssetAcquiredAmount + ); + } + _transferAssetToSender(orders[i].makerAssetData, makerAssetAcquiredAmount); totalWethSpentAmount = totalWethSpentAmount @@ -206,7 +232,10 @@ contract MixinExchangeWrapper is ) { // No taker fee or WETH fee - if (order.takerFee == 0 || order.takerFeeAssetData.equals(order.takerAssetData)) { + if ( + order.takerFee == 0 || + _areUnderlyingAssetsEqual(order.takerFeeAssetData, order.takerAssetData) + ) { // Calculate the remaining amount of takerAsset to sell uint256 remainingTakerAssetFillAmount = LibMath.getPartialAmountCeil( order.takerAssetAmount, @@ -228,7 +257,7 @@ contract MixinExchangeWrapper is makerAssetAcquiredAmount = singleFillResults.makerAssetFilledAmount; // Percentage fee - } else if (order.takerFeeAssetData.equals(order.makerAssetData)) { + } else if (_areUnderlyingAssetsEqual(order.takerFeeAssetData, order.makerAssetData)) { // Calculate the remaining amount of takerAsset to sell uint256 remainingTakerAssetFillAmount = LibMath.getPartialAmountCeil( order.takerAssetAmount, @@ -277,6 +306,8 @@ contract MixinExchangeWrapper is uint256 totalMakerAssetAcquiredAmount ) { + bytes4 erc20BridgeProxyId = IAssetData(address(0)).ERC20Bridge.selector; + uint256 ordersLength = orders.length; for (uint256 i = 0; i != ordersLength; i++) { // Preemptively skip to avoid division by zero in _marketBuySingleOrder @@ -287,6 +318,15 @@ contract MixinExchangeWrapper is uint256 remainingMakerAssetFillAmount = makerAssetBuyAmount .safeSub(totalMakerAssetAcquiredAmount); + // If the maker asset is ERC20Bridge, take a snapshot of the Forwarder contract's balance. + bytes4 makerAssetProxyId = orders[i].makerAssetData.readBytes4(0); + address tokenAddress; + uint256 balanceBefore; + if (makerAssetProxyId == erc20BridgeProxyId) { + tokenAddress = orders[i].makerAssetData.readAddress(16); + balanceBefore = IERC20Token(tokenAddress).balanceOf(address(this)); + } + ( uint256 wethSpentAmount, uint256 makerAssetAcquiredAmount @@ -296,6 +336,15 @@ contract MixinExchangeWrapper is remainingMakerAssetFillAmount ); + // Account for the ERC20Bridge transfering more of the maker asset than expected. + if (makerAssetProxyId == erc20BridgeProxyId) { + uint256 balanceAfter = IERC20Token(tokenAddress).balanceOf(address(this)); + makerAssetAcquiredAmount = LibSafeMath.max256( + balanceAfter.safeSub(balanceBefore), + makerAssetAcquiredAmount + ); + } + _transferAssetToSender(orders[i].makerAssetData, makerAssetAcquiredAmount); totalWethSpentAmount = totalWethSpentAmount @@ -316,4 +365,36 @@ contract MixinExchangeWrapper is )); } } + + /// @dev Checks whether one asset is effectively equal to another asset. + /// This is the case if they have the same ERC20Proxy/ERC20BridgeProxy asset data, or if + /// one is the ERC20Bridge equivalent of the other. + /// @param assetData1 Byte array encoded for the takerFee asset proxy. + /// @param assetData2 Byte array encoded for the maker asset proxy. + /// @return areEqual Whether or not the underlying assets are equal. + function _areUnderlyingAssetsEqual( + bytes memory assetData1, + bytes memory assetData2 + ) + internal + pure + returns (bool) + { + bytes4 assetProxyId1 = assetData1.readBytes4(0); + bytes4 assetProxyId2 = assetData2.readBytes4(0); + bytes4 erc20ProxyId = IAssetData(address(0)).ERC20Token.selector; + bytes4 erc20BridgeProxyId = IAssetData(address(0)).ERC20Bridge.selector; + + if ( + (assetProxyId1 == erc20ProxyId || assetProxyId1 == erc20BridgeProxyId) && + (assetProxyId2 == erc20ProxyId || assetProxyId2 == erc20BridgeProxyId) + ) { + // Compare the underlying token addresses. + address token1 = assetData1.readAddress(16); + address token2 = assetData2.readAddress(16); + return (token1 == token2); + } else { + return false; + } + } } diff --git a/contracts/exchange-forwarder/contracts/src/MixinForwarderCore.sol b/contracts/exchange-forwarder/contracts/src/MixinForwarderCore.sol index dae33e4e29..8b1ba3b804 100644 --- a/contracts/exchange-forwarder/contracts/src/MixinForwarderCore.sol +++ b/contracts/exchange-forwarder/contracts/src/MixinForwarderCore.sol @@ -24,6 +24,7 @@ import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol"; import "./libs/LibConstants.sol"; import "./libs/LibForwarderRichErrors.sol"; import "./interfaces/IAssets.sol"; @@ -46,7 +47,7 @@ contract MixinForwarderCore is constructor () public { - address proxyAddress = EXCHANGE.getAssetProxy(ERC20_DATA_ID); + address proxyAddress = EXCHANGE.getAssetProxy(IAssetData(address(0)).ERC20Token.selector); if (proxyAddress == address(0)) { LibRichErrors.rrevert(LibForwarderRichErrors.UnregisteredAssetProxyError()); } diff --git a/contracts/exchange-forwarder/contracts/src/libs/LibConstants.sol b/contracts/exchange-forwarder/contracts/src/libs/LibConstants.sol index 8be43049d5..8a195e5428 100644 --- a/contracts/exchange-forwarder/contracts/src/libs/LibConstants.sol +++ b/contracts/exchange-forwarder/contracts/src/libs/LibConstants.sol @@ -27,8 +27,6 @@ contract LibConstants { using LibBytes for bytes; - bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)")); - bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256)")); uint256 constant internal MAX_UINT = 2**256 - 1; uint256 constant internal PERCENTAGE_DENOMINATOR = 10**18; uint256 constant internal MAX_FEE_PERCENTAGE = 5 * PERCENTAGE_DENOMINATOR / 100; // 5% @@ -36,19 +34,15 @@ contract LibConstants { // solhint-disable var-name-mixedcase IExchange internal EXCHANGE; IEtherToken internal ETHER_TOKEN; - bytes internal WETH_ASSET_DATA; // solhint-enable var-name-mixedcase constructor ( address _exchange, - bytes memory _wethAssetData + address _weth ) public { EXCHANGE = IExchange(_exchange); - WETH_ASSET_DATA = _wethAssetData; - - address etherToken = _wethAssetData.readAddress(16); - ETHER_TOKEN = IEtherToken(etherToken); + ETHER_TOKEN = IEtherToken(_weth); } } diff --git a/contracts/exchange-forwarder/contracts/src/libs/LibForwarderRichErrors.sol b/contracts/exchange-forwarder/contracts/src/libs/LibForwarderRichErrors.sol index 07418a4dfd..977418ac87 100644 --- a/contracts/exchange-forwarder/contracts/src/libs/LibForwarderRichErrors.sol +++ b/contracts/exchange-forwarder/contracts/src/libs/LibForwarderRichErrors.sol @@ -18,8 +18,6 @@ pragma solidity ^0.5.9; -import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; - library LibForwarderRichErrors { diff --git a/contracts/exchange-forwarder/contracts/test/TestForwarder.sol b/contracts/exchange-forwarder/contracts/test/TestForwarder.sol new file mode 100644 index 0000000000..593597da32 --- /dev/null +++ b/contracts/exchange-forwarder/contracts/test/TestForwarder.sol @@ -0,0 +1,63 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "../src/MixinExchangeWrapper.sol"; +import "../src/libs/LibConstants.sol"; + + +contract TestForwarder is + LibConstants, + MixinExchangeWrapper +{ + // solhint-disable no-empty-blocks + constructor () + public + LibConstants( + address(0), + address(0) + ) + {} + + function areUnderlyingAssetsEqual( + bytes memory assetData1, + bytes memory assetData2 + ) + public + returns (bool) + { + return _areUnderlyingAssetsEqual( + assetData1, + assetData2 + ); + } + + function transferAssetToSender( + bytes memory assetData, + uint256 amount + ) + public + { + _transferAssetToSender( + assetData, + amount + ); + } +} diff --git a/contracts/exchange-forwarder/package.json b/contracts/exchange-forwarder/package.json index 6e29617566..0b2a2bb5a6 100644 --- a/contracts/exchange-forwarder/package.json +++ b/contracts/exchange-forwarder/package.json @@ -14,11 +14,12 @@ "build:ts": "tsc -b", "build:ci": "yarn build", "pre_build": "run-s compile contracts:gen generate_contract_wrappers contracts:copy", - "test": "echo !!! Tests have been relocated to @0x/contracts-integrations !!!", + "test": "yarn run_mocha", "rebuild_and_test": "run-s build test", "test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov", "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html", "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha", + "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", "compile": "sol-compiler", "watch": "sol-compiler -w", "clean": "shx rm -rf lib test/generated-artifacts test/generated-wrappers generated-artifacts generated-wrappers", @@ -38,7 +39,7 @@ }, "config": { "publicInterfaceContracts": "Forwarder", - "abis": "./test/generated-artifacts/@(Forwarder|IAssets|IForwarder|IForwarderCore|LibConstants|LibForwarderRichErrors|MixinAssets|MixinExchangeWrapper|MixinForwarderCore|MixinWeth).json", + "abis": "./test/generated-artifacts/@(Forwarder|IAssets|IForwarder|IForwarderCore|LibConstants|LibForwarderRichErrors|MixinAssets|MixinExchangeWrapper|MixinForwarderCore|MixinWeth|TestForwarder).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/exchange-forwarder/test/artifacts.ts b/contracts/exchange-forwarder/test/artifacts.ts index 2e786fdfa4..4aee7980bd 100644 --- a/contracts/exchange-forwarder/test/artifacts.ts +++ b/contracts/exchange-forwarder/test/artifacts.ts @@ -15,6 +15,7 @@ import * as MixinAssets from '../test/generated-artifacts/MixinAssets.json'; import * as MixinExchangeWrapper from '../test/generated-artifacts/MixinExchangeWrapper.json'; import * as MixinForwarderCore from '../test/generated-artifacts/MixinForwarderCore.json'; import * as MixinWeth from '../test/generated-artifacts/MixinWeth.json'; +import * as TestForwarder from '../test/generated-artifacts/TestForwarder.json'; export const artifacts = { Forwarder: Forwarder as ContractArtifact, MixinAssets: MixinAssets as ContractArtifact, @@ -26,4 +27,5 @@ export const artifacts = { IForwarderCore: IForwarderCore as ContractArtifact, LibConstants: LibConstants as ContractArtifact, LibForwarderRichErrors: LibForwarderRichErrors as ContractArtifact, + TestForwarder: TestForwarder as ContractArtifact, }; diff --git a/contracts/exchange-forwarder/test/asset_test.ts b/contracts/exchange-forwarder/test/asset_test.ts new file mode 100644 index 0000000000..77fa1c8c3e --- /dev/null +++ b/contracts/exchange-forwarder/test/asset_test.ts @@ -0,0 +1,184 @@ +import { IAssetDataContract } from '@0x/contracts-asset-proxy'; +import { + artifacts as ERC20Artifacts, + DummyERC20TokenContract, + ERC20TokenEvents, + ERC20TokenTransferEventArgs, +} from '@0x/contracts-erc20'; +import { + artifacts as ERC721Artifacts, + DummyERC721TokenContract, + ERC721TokenEvents, + ERC721TokenTransferEventArgs, +} from '@0x/contracts-erc721'; +import { + blockchainTests, + constants, + expect, + getRandomInteger, + hexRandom, + hexSlice, + randomAddress, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; +import { BigNumber } from '@0x/utils'; + +import { ForwarderRevertErrors } from '../src'; + +import { artifacts } from './artifacts'; +import { TestForwarderContract } from './wrappers'; + +blockchainTests('Supported asset type unit tests', env => { + let forwarder: TestForwarderContract; + let assetDataEncoder: IAssetDataContract; + let bridgeAddress: string; + let bridgeData: string; + let receiver: string; + + let erc20Token: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; + let nftId: BigNumber; + + let erc20AssetData: string; + let erc721AssetData: string; + let erc20BridgeAssetData: string; + + before(async () => { + [receiver] = await env.getAccountAddressesAsync(); + assetDataEncoder = new IAssetDataContract(constants.NULL_ADDRESS, env.provider); + + forwarder = await TestForwarderContract.deployFrom0xArtifactAsync( + artifacts.TestForwarder, + env.provider, + env.txDefaults, + { ...artifacts, ...ERC20Artifacts, ...ERC721Artifacts }, + ); + + erc20Token = await DummyERC20TokenContract.deployFrom0xArtifactAsync( + ERC20Artifacts.DummyERC20Token, + env.provider, + env.txDefaults, + ERC20Artifacts, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + constants.DUMMY_TOKEN_DECIMALS, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ); + erc20AssetData = assetDataEncoder.ERC20Token(erc20Token.address).getABIEncodedTransactionData(); + + erc721Token = await DummyERC721TokenContract.deployFrom0xArtifactAsync( + ERC721Artifacts.DummyERC721Token, + env.provider, + env.txDefaults, + ERC721Artifacts, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + ); + nftId = getRandomInteger(constants.ZERO_AMOUNT, constants.MAX_UINT256); + erc721AssetData = assetDataEncoder.ERC721Token(erc721Token.address, nftId).getABIEncodedTransactionData(); + + bridgeAddress = randomAddress(); + bridgeData = hexRandom(); + erc20BridgeAssetData = assetDataEncoder + .ERC20Bridge(erc20Token.address, bridgeAddress, bridgeData) + .getABIEncodedTransactionData(); + }); + + describe('_areUnderlyingAssetsEqual', () => { + it('returns true if assetData1 == assetData2 are ERC20', async () => { + const result = await forwarder.areUnderlyingAssetsEqual(erc20AssetData, erc20AssetData).callAsync(); + expect(result).to.be.true(); + }); + it('returns true if assetData1 == assetData2 are ERC20Bridge', async () => { + const result = await forwarder + .areUnderlyingAssetsEqual(erc20BridgeAssetData, erc20BridgeAssetData) + .callAsync(); + expect(result).to.be.true(); + }); + it('returns true if assetData2 is the ERC20Bridge equivalent of assetData1', async () => { + const result = await forwarder.areUnderlyingAssetsEqual(erc20AssetData, erc20BridgeAssetData).callAsync(); + expect(result).to.be.true(); + }); + it('returns false if assetData1 != assetData2 are ERC20', async () => { + const differentERC20AssetData = assetDataEncoder.ERC20Token(randomAddress()).getABIEncodedTransactionData(); + const result = await forwarder + .areUnderlyingAssetsEqual(erc20AssetData, differentERC20AssetData) + .callAsync(); + expect(result).to.be.false(); + }); + it('returns false if assetData1 is ERC20 and assetData2 is ERC721', async () => { + const result = await forwarder.areUnderlyingAssetsEqual(erc20AssetData, erc721AssetData).callAsync(); + expect(result).to.be.false(); + }); + it('returns false if assetData2 is ERC20Bridge, but for a different token than assetData1', async () => { + const mismatchedErc20BridgeAssetData = assetDataEncoder + .ERC20Bridge(randomAddress(), bridgeAddress, bridgeData) + .getABIEncodedTransactionData(); + const result = await forwarder + .areUnderlyingAssetsEqual(erc20AssetData, mismatchedErc20BridgeAssetData) + .callAsync(); + expect(result).to.be.false(); + }); + it('returns false if assetData1 == assetData2 are ERC721', async () => { + const result = await forwarder.areUnderlyingAssetsEqual(erc721AssetData, erc721AssetData).callAsync(); + expect(result).to.be.false(); + }); + }); + + describe('_transferAssetToSender', () => { + const TRANSFER_AMOUNT = new BigNumber(1); + before(async () => { + await erc20Token + .setBalance(forwarder.address, constants.INITIAL_ERC20_BALANCE) + .awaitTransactionSuccessAsync(); + await erc721Token.mint(forwarder.address, nftId).awaitTransactionSuccessAsync(); + }); + + it('transfers an ERC20 token given ERC20 assetData', async () => { + const txReceipt = await forwarder + .transferAssetToSender(erc20AssetData, TRANSFER_AMOUNT) + .awaitTransactionSuccessAsync({ from: receiver }); + verifyEventsFromLogs( + txReceipt.logs, + [{ _from: forwarder.address, _to: receiver, _value: TRANSFER_AMOUNT }], + ERC20TokenEvents.Transfer, + ); + }); + it('transfers an ERC721 token given ERC721 assetData and amount == 1', async () => { + const txReceipt = await forwarder + .transferAssetToSender(erc721AssetData, TRANSFER_AMOUNT) + .awaitTransactionSuccessAsync({ from: receiver }); + verifyEventsFromLogs( + txReceipt.logs, + [{ _from: forwarder.address, _to: receiver, _tokenId: nftId }], + ERC721TokenEvents.Transfer, + ); + }); + it('reverts if attempting to transfer an ERC721 token with amount != 1', async () => { + const invalidAmount = new BigNumber(2); + const tx = forwarder + .transferAssetToSender(erc721AssetData, invalidAmount) + .awaitTransactionSuccessAsync({ from: receiver }); + const expectedError = new ForwarderRevertErrors.Erc721AmountMustEqualOneError(invalidAmount); + return expect(tx).to.revertWith(expectedError); + }); + it('transfers an ERC20 token given ERC20Bridge assetData', async () => { + const txReceipt = await forwarder + .transferAssetToSender(erc20BridgeAssetData, TRANSFER_AMOUNT) + .awaitTransactionSuccessAsync({ from: receiver }); + verifyEventsFromLogs( + txReceipt.logs, + [{ _from: forwarder.address, _to: receiver, _value: TRANSFER_AMOUNT }], + ERC20TokenEvents.Transfer, + ); + }); + it('reverts if assetData is unsupported', async () => { + const randomBytes = hexRandom(); + const tx = forwarder + .transferAssetToSender(randomBytes, TRANSFER_AMOUNT) + .awaitTransactionSuccessAsync({ from: receiver }); + const expectedError = new ForwarderRevertErrors.UnsupportedAssetProxyError(hexSlice(randomBytes, 0, 4)); + return expect(tx).to.revertWith(expectedError); + }); + }); +}); diff --git a/contracts/exchange-forwarder/test/wrappers.ts b/contracts/exchange-forwarder/test/wrappers.ts index fd8cbc5af8..22b048ce75 100644 --- a/contracts/exchange-forwarder/test/wrappers.ts +++ b/contracts/exchange-forwarder/test/wrappers.ts @@ -13,3 +13,4 @@ export * from '../test/generated-wrappers/mixin_assets'; export * from '../test/generated-wrappers/mixin_exchange_wrapper'; export * from '../test/generated-wrappers/mixin_forwarder_core'; export * from '../test/generated-wrappers/mixin_weth'; +export * from '../test/generated-wrappers/test_forwarder'; diff --git a/contracts/exchange-forwarder/tsconfig.json b/contracts/exchange-forwarder/tsconfig.json index 8186631147..94a8af37fc 100644 --- a/contracts/exchange-forwarder/tsconfig.json +++ b/contracts/exchange-forwarder/tsconfig.json @@ -13,7 +13,8 @@ "test/generated-artifacts/MixinAssets.json", "test/generated-artifacts/MixinExchangeWrapper.json", "test/generated-artifacts/MixinForwarderCore.json", - "test/generated-artifacts/MixinWeth.json" + "test/generated-artifacts/MixinWeth.json", + "test/generated-artifacts/TestForwarder.json" ], "exclude": ["./deploy/solc/solc_bin"] } diff --git a/contracts/integrations/CHANGELOG.json b/contracts/integrations/CHANGELOG.json index 13729d51b0..12a4aa8a74 100644 --- a/contracts/integrations/CHANGELOG.json +++ b/contracts/integrations/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.0.3-beta.2", + "changes": [ + { + "note": "Forwader <> ERC20Bridge integration tests", + "pr": 2356 + } + ] + }, { "version": "1.0.3-beta.1", "changes": [ diff --git a/contracts/integrations/contracts/test/TestEth2Dai.sol b/contracts/integrations/contracts/test/TestEth2Dai.sol new file mode 100644 index 0000000000..011906472c --- /dev/null +++ b/contracts/integrations/contracts/test/TestEth2Dai.sol @@ -0,0 +1,59 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IEth2Dai.sol"; +import "@0x/contracts-erc20/contracts/test/DummyERC20Token.sol"; + + +contract TestEth2Dai is + IEth2Dai +{ + uint256 private _excessBuyAmount; + + function setExcessBuyAmount(uint256 amount) + external + { + _excessBuyAmount = amount; + } + + function sellAllAmount( + address sellTokenAddress, + uint256 sellTokenAmount, + address buyTokenAddress, + uint256 minimumFillAmount + ) + external + returns (uint256 fillAmount) + { + DummyERC20Token(sellTokenAddress).transferFrom( + msg.sender, + address(this), + sellTokenAmount + ); + DummyERC20Token buyToken = DummyERC20Token(buyTokenAddress); + buyToken.mint(minimumFillAmount + _excessBuyAmount); + buyToken.transfer( + msg.sender, + minimumFillAmount + _excessBuyAmount + ); + return minimumFillAmount + _excessBuyAmount; + } +} diff --git a/contracts/integrations/contracts/test/TestEth2DaiBridge.sol b/contracts/integrations/contracts/test/TestEth2DaiBridge.sol new file mode 100644 index 0000000000..4029e9f110 --- /dev/null +++ b/contracts/integrations/contracts/test/TestEth2DaiBridge.sol @@ -0,0 +1,45 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol"; +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IEth2Dai.sol"; + + +contract TestEth2DaiBridge is + Eth2DaiBridge +{ + // solhint-disable var-name-mixedcase + address public TEST_ETH2DAI_ADDRESS; + + constructor (address testEth2Dai) + public + { + TEST_ETH2DAI_ADDRESS = testEth2Dai; + } + + function _getEth2DaiContract() + internal + view + returns (IEth2Dai exchange) + { + return IEth2Dai(TEST_ETH2DAI_ADDRESS); + } +} diff --git a/contracts/integrations/contracts/test/TestUniswapBridge.sol b/contracts/integrations/contracts/test/TestUniswapBridge.sol new file mode 100644 index 0000000000..1154218d78 --- /dev/null +++ b/contracts/integrations/contracts/test/TestUniswapBridge.sol @@ -0,0 +1,59 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-asset-proxy/contracts/src/bridges/UniswapBridge.sol"; +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IUniswapExchangeFactory.sol"; +import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; + + +contract TestUniswapBridge is + UniswapBridge +{ + // solhint-disable var-name-mixedcase + address public TEST_WETH_ADDRESS; + address public TEST_UNISWAP_EXCHANGE_FACTORY_ADDRESS; + + constructor ( + address testWeth, + address testUniswapExchangeFactory + ) + public + { + TEST_WETH_ADDRESS = testWeth; + TEST_UNISWAP_EXCHANGE_FACTORY_ADDRESS = testUniswapExchangeFactory; + } + + function getWethContract() + public + view + returns (IEtherToken token) + { + return IEtherToken(TEST_WETH_ADDRESS); + } + + function getUniswapExchangeFactoryContract() + public + view + returns (IUniswapExchangeFactory factory) + { + return IUniswapExchangeFactory(TEST_UNISWAP_EXCHANGE_FACTORY_ADDRESS); + } +} diff --git a/contracts/integrations/contracts/test/TestUniswapExchange.sol b/contracts/integrations/contracts/test/TestUniswapExchange.sol new file mode 100644 index 0000000000..256d58a035 --- /dev/null +++ b/contracts/integrations/contracts/test/TestUniswapExchange.sol @@ -0,0 +1,109 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IUniswapExchange.sol"; +import "@0x/contracts-erc20/contracts/test/DummyERC20Token.sol"; + + +contract TestUniswapExchange is + IUniswapExchange +{ + DummyERC20Token public token; + uint256 private _excessBuyAmount; + + constructor(address _tokenAddress) public { + token = DummyERC20Token(_tokenAddress); + } + + // solhint-disable no-empty-blocks + /// @dev Used to receive ETH for testing. + function topUpEth() + external + payable + {} + + function setExcessBuyAmount(uint256 amount) + external + { + _excessBuyAmount = amount; + } + + function ethToTokenTransferInput( + uint256 minTokensBought, + uint256, /* deadline */ + address recipient + ) + external + payable + returns (uint256 tokensBought) + { + token.mint(minTokensBought + _excessBuyAmount); + token.transfer(recipient, minTokensBought + _excessBuyAmount); + return minTokensBought + _excessBuyAmount; + } + + function tokenToEthSwapInput( + uint256 tokensSold, + uint256 minEthBought, + uint256 /* deadline */ + ) + external + returns (uint256 ethBought) + { + token.transferFrom( + msg.sender, + address(this), + tokensSold + ); + msg.sender.transfer(minEthBought + _excessBuyAmount); + return minEthBought + _excessBuyAmount; + } + + function tokenToTokenTransferInput( + uint256 tokensSold, + uint256 minTokensBought, + uint256, /* minEthBought */ + uint256, /* deadline */ + address recipient, + address toTokenAddress + ) + external + returns (uint256 tokensBought) + { + token.transferFrom( + msg.sender, + address(this), + tokensSold + ); + DummyERC20Token toToken = DummyERC20Token(toTokenAddress); + toToken.mint(minTokensBought + _excessBuyAmount); + toToken.transfer(recipient, minTokensBought + _excessBuyAmount); + return minTokensBought + _excessBuyAmount; + } + + function toTokenAddress() + external + view + returns (address _tokenAddress) + { + return address(token); + } +} diff --git a/contracts/integrations/contracts/test/TestUniswapExchangeFactory.sol b/contracts/integrations/contracts/test/TestUniswapExchangeFactory.sol new file mode 100644 index 0000000000..04f0654c02 --- /dev/null +++ b/contracts/integrations/contracts/test/TestUniswapExchangeFactory.sol @@ -0,0 +1,52 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-asset-proxy/contracts/src/bridges/UniswapBridge.sol"; +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IUniswapExchangeFactory.sol"; + + +contract TestUniswapExchangeFactory is + IUniswapExchangeFactory +{ + // Token address to UniswapExchange address. + mapping (address => address) private _testExchanges; + + /// @dev Create a token and exchange (if they don't exist) for a new token + /// and sets the exchange revert and fill behavior. + /// @param tokenAddress The token address. + function addExchange( + address tokenAddress, + address exchangeAddress + ) + external + { + _testExchanges[tokenAddress] = exchangeAddress; + } + + /// @dev `IUniswapExchangeFactory.getExchange` + function getExchange(address tokenAddress) + external + view + returns (address) + { + return _testExchanges[tokenAddress]; + } +} diff --git a/contracts/integrations/package.json b/contracts/integrations/package.json index 06a7bf0f12..51b5e1f397 100644 --- a/contracts/integrations/package.json +++ b/contracts/integrations/package.json @@ -37,7 +37,7 @@ }, "config": { "publicInterfaceContracts": "TestFramework", - "abis": "./test/generated-artifacts/@(TestFramework).json", + "abis": "./test/generated-artifacts/@(TestEth2Dai|TestEth2DaiBridge|TestFramework|TestUniswapBridge|TestUniswapExchange|TestUniswapExchangeFactory).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/integrations/test/artifacts.ts b/contracts/integrations/test/artifacts.ts index 8ea0d872cf..29b07a6d02 100644 --- a/contracts/integrations/test/artifacts.ts +++ b/contracts/integrations/test/artifacts.ts @@ -5,5 +5,17 @@ */ import { ContractArtifact } from 'ethereum-types'; +import * as TestEth2Dai from '../test/generated-artifacts/TestEth2Dai.json'; +import * as TestEth2DaiBridge from '../test/generated-artifacts/TestEth2DaiBridge.json'; import * as TestFramework from '../test/generated-artifacts/TestFramework.json'; -export const artifacts = { TestFramework: TestFramework as ContractArtifact }; +import * as TestUniswapBridge from '../test/generated-artifacts/TestUniswapBridge.json'; +import * as TestUniswapExchange from '../test/generated-artifacts/TestUniswapExchange.json'; +import * as TestUniswapExchangeFactory from '../test/generated-artifacts/TestUniswapExchangeFactory.json'; +export const artifacts = { + TestEth2Dai: TestEth2Dai as ContractArtifact, + TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, + TestFramework: TestFramework as ContractArtifact, + TestUniswapBridge: TestUniswapBridge as ContractArtifact, + TestUniswapExchange: TestUniswapExchange as ContractArtifact, + TestUniswapExchangeFactory: TestUniswapExchangeFactory as ContractArtifact, +}; diff --git a/contracts/integrations/test/bridges/deploy_eth2dai_bridge.ts b/contracts/integrations/test/bridges/deploy_eth2dai_bridge.ts new file mode 100644 index 0000000000..6fb1693594 --- /dev/null +++ b/contracts/integrations/test/bridges/deploy_eth2dai_bridge.ts @@ -0,0 +1,31 @@ +import { artifacts as ERC20Artifacts } from '@0x/contracts-erc20'; +import { BlockchainTestsEnvironment } from '@0x/contracts-test-utils'; + +import { artifacts } from '../artifacts'; +import { DeploymentManager } from '../framework/deployment_manager'; +import { TestEth2DaiBridgeContract, TestEth2DaiContract } from '../wrappers'; + +/** + * Deploys test Eth2Dai exchange and bridge contracts configured to work alongside the provided `deployment`. + */ +export async function deployEth2DaiBridgeAsync( + deployment: DeploymentManager, + environment: BlockchainTestsEnvironment, +): Promise<[TestEth2DaiBridgeContract, TestEth2DaiContract]> { + const eth2Dai = await TestEth2DaiContract.deployFrom0xArtifactAsync( + artifacts.TestEth2Dai, + environment.provider, + deployment.txDefaults, + artifacts, + ); + + const eth2DaiBridge = await TestEth2DaiBridgeContract.deployFrom0xArtifactAsync( + artifacts.TestEth2DaiBridge, + environment.provider, + deployment.txDefaults, + { ...ERC20Artifacts, ...artifacts }, + eth2Dai.address, + ); + + return [eth2DaiBridge, eth2Dai]; +} diff --git a/contracts/integrations/test/bridges/deploy_uniswap_bridge.ts b/contracts/integrations/test/bridges/deploy_uniswap_bridge.ts new file mode 100644 index 0000000000..fb1e9b2647 --- /dev/null +++ b/contracts/integrations/test/bridges/deploy_uniswap_bridge.ts @@ -0,0 +1,51 @@ +import { artifacts as ERC20Artifacts } from '@0x/contracts-erc20'; +import { BlockchainTestsEnvironment } from '@0x/contracts-test-utils'; + +import { artifacts } from '../artifacts'; +import { DeploymentManager } from '../framework/deployment_manager'; +import { + TestUniswapBridgeContract, + TestUniswapExchangeContract, + TestUniswapExchangeFactoryContract, +} from '../wrappers'; + +/** + * Deploys test Uniswap exchanges for the given tokens, a test UniswapExchangeFactory, and a test + * bridge contract configured to work alongside the provided `deployment`. + */ +export async function deployUniswapBridgeAsync( + deployment: DeploymentManager, + environment: BlockchainTestsEnvironment, + tokenAddresses: string[], +): Promise<[TestUniswapBridgeContract, TestUniswapExchangeContract[], TestUniswapExchangeFactoryContract]> { + const uniswapExchangeFactory = await TestUniswapExchangeFactoryContract.deployFrom0xArtifactAsync( + artifacts.TestUniswapExchangeFactory, + environment.provider, + deployment.txDefaults, + artifacts, + ); + + const uniswapExchanges = []; + for (const tokenAddress of tokenAddresses) { + const uniswapExchange = await TestUniswapExchangeContract.deployFrom0xArtifactAsync( + artifacts.TestUniswapExchange, + environment.provider, + deployment.txDefaults, + artifacts, + tokenAddress, + ); + await uniswapExchangeFactory.addExchange(tokenAddress, uniswapExchange.address).awaitTransactionSuccessAsync(); + uniswapExchanges.push(uniswapExchange); + } + + const uniswapBridge = await TestUniswapBridgeContract.deployFrom0xArtifactAsync( + artifacts.TestUniswapBridge, + environment.provider, + deployment.txDefaults, + { ...ERC20Artifacts, ...artifacts }, + deployment.tokens.weth.address, + uniswapExchangeFactory.address, + ); + + return [uniswapBridge, uniswapExchanges, uniswapExchangeFactory]; +} diff --git a/contracts/integrations/test/coordinator/coordinator_test.ts b/contracts/integrations/test/coordinator/coordinator_test.ts index 40dd797619..3ce9b50669 100644 --- a/contracts/integrations/test/coordinator/coordinator_test.ts +++ b/contracts/integrations/test/coordinator/coordinator_test.ts @@ -1,5 +1,4 @@ import { CoordinatorContract, CoordinatorRevertErrors, SignedCoordinatorApproval } from '@0x/contracts-coordinator'; -import { DevUtilsContract } from '@0x/contracts-dev-utils'; import { ExchangeCancelEventArgs, ExchangeCancelUpToEventArgs, @@ -15,7 +14,6 @@ import { hexConcat, hexSlice, orderHashUtils, - provider, transactionHashUtils, verifyEvents, } from '@0x/contracts-test-utils'; @@ -38,7 +36,6 @@ blockchainTests.resets('Coordinator integration tests', env => { let deployment: DeploymentManager; let coordinator: CoordinatorContract; let balanceStore: BlockchainBalanceStore; - let devUtils: DevUtilsContract; let maker: Maker; let taker: Actor; @@ -51,7 +48,6 @@ blockchainTests.resets('Coordinator integration tests', env => { numErc1155TokensToDeploy: 0, }); coordinator = await deployCoordinatorAsync(deployment, env); - devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider); const [makerToken, takerToken, makerFeeToken, takerFeeToken] = deployment.tokens.erc20; @@ -109,7 +105,7 @@ blockchainTests.resets('Coordinator integration tests', env => { msgValue?: BigNumber, ): Promise { let remainingValue = msgValue || constants.ZERO_AMOUNT; - const localBalanceStore = LocalBalanceStore.create(devUtils, balanceStore); + const localBalanceStore = LocalBalanceStore.create(balanceStore); // Transaction gas cost localBalanceStore.burnGas(txReceipt.from, DeploymentManager.gasPrice.times(txReceipt.gasUsed)); diff --git a/contracts/integrations/test/exchange/core_test.ts b/contracts/integrations/test/exchange/core_test.ts index 4200d4dece..344ae06fda 100644 --- a/contracts/integrations/test/exchange/core_test.ts +++ b/contracts/integrations/test/exchange/core_test.ts @@ -221,7 +221,6 @@ blockchainTests.resets('Exchange core', () => { }; fillOrderWrapper = new FillOrderWrapper( exchange, - devUtils, { makerAddress, takerAddress, feeRecipientAddress }, tokenContracts, tokenIds, diff --git a/contracts/integrations/test/exchange/exchange_wrapper_test.ts b/contracts/integrations/test/exchange/exchange_wrapper_test.ts index c80fbb214e..ef057c6539 100644 --- a/contracts/integrations/test/exchange/exchange_wrapper_test.ts +++ b/contracts/integrations/test/exchange/exchange_wrapper_test.ts @@ -112,7 +112,7 @@ blockchainTests.resets('Exchange wrappers', env => { await blockchainBalances.updateBalancesAsync(); - initialLocalBalances = LocalBalanceStore.create(deployment.devUtils, blockchainBalances); + initialLocalBalances = LocalBalanceStore.create(blockchainBalances); wethAssetData = deployment.assetDataEncoder .ERC20Token(deployment.tokens.weth.address) @@ -120,7 +120,7 @@ blockchainTests.resets('Exchange wrappers', env => { }); beforeEach(async () => { - localBalances = LocalBalanceStore.create(deployment.devUtils, initialLocalBalances); + localBalances = LocalBalanceStore.create(initialLocalBalances); }); after(async () => { diff --git a/contracts/integrations/test/exchange/fill_order_wrapper.ts b/contracts/integrations/test/exchange/fill_order_wrapper.ts index cd58ca98cb..d8d996aac2 100644 --- a/contracts/integrations/test/exchange/fill_order_wrapper.ts +++ b/contracts/integrations/test/exchange/fill_order_wrapper.ts @@ -1,4 +1,3 @@ -import { DevUtilsContract } from '@0x/contracts-dev-utils'; import { ExchangeContract } from '@0x/contracts-exchange'; import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs'; import { @@ -56,65 +55,6 @@ export class FillOrderWrapper { return events.map(event => _.pick(event, fieldsOfInterest)) as FillEventArgs[]; } - /** - * Locally simulates filling an order. - * @param txReceipt Transaction receipt from the actual fill, needed to update eth balance - * @param signedOrder The order being filled. - * @param takerAddress Address of taker (the address who matched the two orders) - * @param opts Optionally specifies the amount to fill. - * @param initBalanceStore Account balances prior to the fill. - * @return The expected account balances, fill results, and fill events. - */ - public async simulateFillOrderAsync( - txReceipt: TransactionReceiptWithDecodedLogs, - signedOrder: SignedOrder, - takerAddress: string, - initBalanceStore: BalanceStore, - opts: { takerAssetFillAmount?: BigNumber } = {}, - ): Promise<[FillResults, FillEventArgs, BalanceStore]> { - const balanceStore = LocalBalanceStore.create(this._devUtils, initBalanceStore); - const takerAssetFillAmount = - opts.takerAssetFillAmount !== undefined ? opts.takerAssetFillAmount : signedOrder.takerAssetAmount; - // TODO(jalextowle): Change this if the integration tests take protocol fees into account. - const fillResults = LibReferenceFunctions.calculateFillResults( - signedOrder, - takerAssetFillAmount, - constants.ZERO_AMOUNT, - constants.ZERO_AMOUNT, - ); - const fillEvent = FillOrderWrapper.simulateFillEvent(signedOrder, takerAddress, fillResults); - // Taker -> Maker - await balanceStore.transferAssetAsync( - takerAddress, - signedOrder.makerAddress, - fillResults.takerAssetFilledAmount, - signedOrder.takerAssetData, - ); - // Maker -> Taker - await balanceStore.transferAssetAsync( - signedOrder.makerAddress, - takerAddress, - fillResults.makerAssetFilledAmount, - signedOrder.makerAssetData, - ); - // Taker -> Fee Recipient - await balanceStore.transferAssetAsync( - takerAddress, - signedOrder.feeRecipientAddress, - fillResults.takerFeePaid, - signedOrder.takerFeeAssetData, - ); - // Maker -> Fee Recipient - await balanceStore.transferAssetAsync( - signedOrder.makerAddress, - signedOrder.feeRecipientAddress, - fillResults.makerFeePaid, - signedOrder.makerFeeAssetData, - ); - balanceStore.burnGas(txReceipt.from, constants.DEFAULT_GAS_PRICE * txReceipt.gasUsed); - return [fillResults, fillEvent, balanceStore]; - } - /** * Constructor. * @param exchangeContract Instance of the deployed exchange contract. @@ -124,7 +64,6 @@ export class FillOrderWrapper { */ public constructor( private readonly _exchange: ExchangeContract, - private readonly _devUtils: DevUtilsContract, tokenOwnersByName: TokenOwnersByName, tokenContractsByName: Partial, tokenIds: Partial, @@ -160,11 +99,13 @@ export class FillOrderWrapper { await this._assertOrderStateAsync(signedOrder, initTakerAssetFilledAmount); // Simulate and execute fill then assert outputs const [fillResults, fillEvent, txReceipt] = await this._fillOrderAsync(signedOrder, from, opts); - const [ - simulatedFillResults, - simulatedFillEvent, - simulatedFinalBalanceStore, - ] = await this.simulateFillOrderAsync(txReceipt, signedOrder, from, this._blockchainBalanceStore, opts); + const [simulatedFillResults, simulatedFillEvent, simulatedFinalBalanceStore] = await simulateFillOrderAsync( + txReceipt, + signedOrder, + from, + this._blockchainBalanceStore, + opts, + ); // Assert state transition expect(simulatedFillResults, 'Fill Results').to.be.deep.equal(fillResults); expect(simulatedFillEvent, 'Fill Events').to.be.deep.equal(fillEvent); @@ -218,3 +159,62 @@ export class FillOrderWrapper { expect(actualStatus, 'order status').to.equal(expectedStatus); } } + +/** + * Locally simulates filling an order. + * @param txReceipt Transaction receipt from the actual fill, needed to update eth balance + * @param signedOrder The order being filled. + * @param takerAddress Address of taker (the address who matched the two orders) + * @param opts Optionally specifies the amount to fill. + * @param initBalanceStore Account balances prior to the fill. + * @return The expected account balances, fill results, and fill events. + */ +async function simulateFillOrderAsync( + txReceipt: TransactionReceiptWithDecodedLogs, + signedOrder: SignedOrder, + takerAddress: string, + initBalanceStore: BalanceStore, + opts: { takerAssetFillAmount?: BigNumber } = {}, +): Promise<[FillResults, FillEventArgs, BalanceStore]> { + const balanceStore = LocalBalanceStore.create(initBalanceStore); + const takerAssetFillAmount = + opts.takerAssetFillAmount !== undefined ? opts.takerAssetFillAmount : signedOrder.takerAssetAmount; + // TODO(jalextowle): Change this if the integration tests take protocol fees into account. + const fillResults = LibReferenceFunctions.calculateFillResults( + signedOrder, + takerAssetFillAmount, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + const fillEvent = FillOrderWrapper.simulateFillEvent(signedOrder, takerAddress, fillResults); + // Taker -> Maker + await balanceStore.transferAssetAsync( + takerAddress, + signedOrder.makerAddress, + fillResults.takerAssetFilledAmount, + signedOrder.takerAssetData, + ); + // Maker -> Taker + await balanceStore.transferAssetAsync( + signedOrder.makerAddress, + takerAddress, + fillResults.makerAssetFilledAmount, + signedOrder.makerAssetData, + ); + // Taker -> Fee Recipient + await balanceStore.transferAssetAsync( + takerAddress, + signedOrder.feeRecipientAddress, + fillResults.takerFeePaid, + signedOrder.takerFeeAssetData, + ); + // Maker -> Fee Recipient + await balanceStore.transferAssetAsync( + signedOrder.makerAddress, + signedOrder.feeRecipientAddress, + fillResults.makerFeePaid, + signedOrder.makerFeeAssetData, + ); + balanceStore.burnGas(txReceipt.from, constants.DEFAULT_GAS_PRICE * txReceipt.gasUsed); + return [fillResults, fillEvent, balanceStore]; +} diff --git a/contracts/integrations/test/exchange/fillorder_test.ts b/contracts/integrations/test/exchange/fillorder_test.ts index 00d83ff85c..7e8bf72457 100644 --- a/contracts/integrations/test/exchange/fillorder_test.ts +++ b/contracts/integrations/test/exchange/fillorder_test.ts @@ -118,7 +118,7 @@ blockchainTests.resets('fillOrder integration tests', env => { msgValue?: BigNumber, ): Promise { let remainingValue = msgValue !== undefined ? msgValue : DeploymentManager.protocolFee; - const localBalanceStore = LocalBalanceStore.create(deployment.devUtils, balanceStore); + const localBalanceStore = LocalBalanceStore.create(balanceStore); // Transaction gas cost localBalanceStore.burnGas(txReceipt.from, DeploymentManager.gasPrice.times(txReceipt.gasUsed)); @@ -266,7 +266,7 @@ blockchainTests.resets('fillOrder integration tests', env => { // Fetch the current balances await balanceStore.updateBalancesAsync(); - const expectedBalances = LocalBalanceStore.create(deployment.devUtils, balanceStore); + const expectedBalances = LocalBalanceStore.create(balanceStore); // End the epoch. This should wrap the staking proxy's ETH balance. const endEpochReceipt = await delegator.endEpochAsync(); diff --git a/contracts/integrations/test/exchange/match_order_tester.ts b/contracts/integrations/test/exchange/match_order_tester.ts index 0d763bb512..b3decd724d 100644 --- a/contracts/integrations/test/exchange/match_order_tester.ts +++ b/contracts/integrations/test/exchange/match_order_tester.ts @@ -168,7 +168,7 @@ export class MatchOrderTester { // Update the blockchain balance store and create a new local balance store // with the same initial balances. await this._blockchainBalanceStore.updateBalancesAsync(); - const localBalanceStore = LocalBalanceStore.create(this._deployment.devUtils, this._blockchainBalanceStore); + const localBalanceStore = LocalBalanceStore.create(this._blockchainBalanceStore); // Execute `batchMatchOrders()` let actualBatchMatchResults; @@ -253,7 +253,7 @@ export class MatchOrderTester { // Update the blockchain balance store and create a new local balance store // with the same initial balances. await this._blockchainBalanceStore.updateBalancesAsync(); - const localBalanceStore = LocalBalanceStore.create(this._deployment.devUtils, this._blockchainBalanceStore); + const localBalanceStore = LocalBalanceStore.create(this._blockchainBalanceStore); // Execute `matchOrders()` let actualMatchResults; diff --git a/contracts/integrations/test/forwarder/bridge_test.ts b/contracts/integrations/test/forwarder/bridge_test.ts new file mode 100644 index 0000000000..22061224c7 --- /dev/null +++ b/contracts/integrations/test/forwarder/bridge_test.ts @@ -0,0 +1,378 @@ +import { IAssetDataContract } from '@0x/contracts-asset-proxy'; +import { DummyERC721TokenContract } from '@0x/contracts-erc721'; +import { ForwarderContract, ForwarderRevertErrors } from '@0x/contracts-exchange-forwarder'; +import { + blockchainTests, + constants, + getLatestBlockTimestampAsync, + hexConcat, + toBaseUnitAmount, +} from '@0x/contracts-test-utils'; +import { generatePseudoRandomSalt } from '@0x/order-utils'; +import { SignatureType, SignedOrder } from '@0x/types'; +import { AbiEncoder, BigNumber } from '@0x/utils'; + +import { deployEth2DaiBridgeAsync } from '../bridges/deploy_eth2dai_bridge'; +import { deployUniswapBridgeAsync } from '../bridges/deploy_uniswap_bridge'; +import { Actor } from '../framework/actors/base'; +import { FeeRecipient } from '../framework/actors/fee_recipient'; +import { Maker } from '../framework/actors/maker'; +import { Taker } from '../framework/actors/taker'; +import { actorAddressesByName } from '../framework/actors/utils'; +import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store'; +import { DeploymentManager } from '../framework/deployment_manager'; +import { TestEth2DaiContract, TestUniswapExchangeContract } from '../wrappers'; + +import { deployForwarderAsync } from './deploy_forwarder'; +import { ForwarderTestFactory } from './forwarder_test_factory'; + +blockchainTests.resets('Forwarder <> ERC20Bridge integration tests', env => { + let deployment: DeploymentManager; + let balanceStore: BlockchainBalanceStore; + let testFactory: ForwarderTestFactory; + + let forwarder: ForwarderContract; + let assetDataEncoder: IAssetDataContract; + let eth2Dai: TestEth2DaiContract; + let uniswapExchange: TestUniswapExchangeContract; + + let erc721Token: DummyERC721TokenContract; + let nftId: BigNumber; + let makerTokenAssetData: string; + let makerFeeTokenAssetData: string; + let eth2DaiBridgeAssetData: string; + let uniswapBridgeAssetData: string; + + let maker: Maker; + let taker: Taker; + let orderFeeRecipient: FeeRecipient; + let forwarderFeeRecipient: FeeRecipient; + + let eth2DaiBridgeOrder: SignedOrder; + let uniswapBridgeOrder: SignedOrder; + + before(async () => { + assetDataEncoder = new IAssetDataContract(constants.NULL_ADDRESS, env.provider); + deployment = await DeploymentManager.deployAsync(env, { + numErc20TokensToDeploy: 2, + numErc721TokensToDeploy: 1, + numErc1155TokensToDeploy: 0, + }); + const [makerToken, makerFeeToken] = deployment.tokens.erc20; + [erc721Token] = deployment.tokens.erc721; + + forwarder = await deployForwarderAsync(deployment, env); + const eth2DaiContracts = await deployEth2DaiBridgeAsync(deployment, env); + const [eth2DaiBridge] = eth2DaiContracts; + [, eth2Dai] = eth2DaiContracts; + const uniswapContracts = await deployUniswapBridgeAsync(deployment, env, [makerToken.address]); + const [uniswapBridge] = uniswapContracts; + [, [uniswapExchange]] = uniswapContracts; + + makerTokenAssetData = assetDataEncoder.ERC20Token(makerToken.address).getABIEncodedTransactionData(); + makerFeeTokenAssetData = assetDataEncoder.ERC20Token(makerFeeToken.address).getABIEncodedTransactionData(); + const wethAssetData = assetDataEncoder + .ERC20Token(deployment.tokens.weth.address) + .getABIEncodedTransactionData(); + + const bridgeDataEncoder = AbiEncoder.create([{ name: 'fromTokenAddress', type: 'address' }]); + const bridgeData = bridgeDataEncoder.encode([deployment.tokens.weth.address]); + eth2DaiBridgeAssetData = assetDataEncoder + .ERC20Bridge(makerToken.address, eth2DaiBridge.address, bridgeData) + .getABIEncodedTransactionData(); + uniswapBridgeAssetData = assetDataEncoder + .ERC20Bridge(makerToken.address, uniswapBridge.address, bridgeData) + .getABIEncodedTransactionData(); + + taker = new Taker({ name: 'Taker', deployment }); + orderFeeRecipient = new FeeRecipient({ + name: 'Order fee recipient', + deployment, + }); + forwarderFeeRecipient = new FeeRecipient({ + name: 'Forwarder fee recipient', + deployment, + }); + + const fifteenMinutesInSeconds = 15 * 60; + const currentBlockTimestamp = await getLatestBlockTimestampAsync(); + const orderDefaults = { + chainId: deployment.chainId, + exchangeAddress: deployment.exchange.address, + takerAddress: constants.NULL_ADDRESS, + feeRecipientAddress: orderFeeRecipient.address, + senderAddress: constants.NULL_ADDRESS, + makerAssetAmount: toBaseUnitAmount(2), + takerAssetAmount: toBaseUnitAmount(1), + takerAssetData: wethAssetData, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + makerFeeAssetData: makerFeeTokenAssetData, + takerFeeAssetData: wethAssetData, + expirationTimeSeconds: new BigNumber(currentBlockTimestamp).plus(fifteenMinutesInSeconds), + salt: generatePseudoRandomSalt(), + signature: hexConcat(SignatureType.Wallet), + }; + eth2DaiBridgeOrder = { + ...orderDefaults, + makerAddress: eth2DaiBridge.address, + makerAssetData: eth2DaiBridgeAssetData, + }; + uniswapBridgeOrder = { + ...orderDefaults, + makerAddress: uniswapBridge.address, + makerAssetData: uniswapBridgeAssetData, + }; + + maker = new Maker({ + name: 'Maker', + deployment, + orderConfig: { ...orderDefaults, makerFee: toBaseUnitAmount(0.01) }, + }); + await maker.configureERC20TokenAsync(makerToken); + await maker.configureERC20TokenAsync(makerFeeToken); + await forwarder.approveMakerAssetProxy(makerTokenAssetData).awaitTransactionSuccessAsync(); + [nftId] = await maker.configureERC721TokenAsync(erc721Token); + + // We need to top up the TestUniswapExchange with some ETH so that it can perform tokenToEthSwapInput + await uniswapExchange.topUpEth().awaitTransactionSuccessAsync({ + from: forwarderFeeRecipient.address, + value: constants.ONE_ETHER.times(10), + }); + + const tokenOwners = { + ...actorAddressesByName([maker, taker, orderFeeRecipient, forwarderFeeRecipient]), + Forwarder: forwarder.address, + StakingProxy: deployment.staking.stakingProxy.address, + }; + const tokenContracts = { + erc20: { makerToken, makerFeeToken, wETH: deployment.tokens.weth }, + erc721: { erc721Token }, + }; + const tokenIds = { erc721: { [erc721Token.address]: [nftId] } }; + balanceStore = new BlockchainBalanceStore(tokenOwners, tokenContracts, tokenIds); + + testFactory = new ForwarderTestFactory(forwarder, deployment, balanceStore, taker, forwarderFeeRecipient); + }); + + after(async () => { + Actor.count = 0; + }); + + describe('marketSellOrdersWithEth', () => { + it('should fully fill a single Eth2DaiBridge order without a taker fee', async () => { + await testFactory.marketSellTestAsync([eth2DaiBridgeOrder], 1); + }); + it('should partially fill a single Eth2DaiBridge order without a taker fee', async () => { + await testFactory.marketSellTestAsync([eth2DaiBridgeOrder], 0.34); + }); + it('should correctly handle excess maker asset acquired from Eth2Dai', async () => { + const bridgeExcessBuyAmount = new BigNumber(1); + await eth2Dai.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync(); + await testFactory.marketSellTestAsync([eth2DaiBridgeOrder], 0.34, { bridgeExcessBuyAmount }); + }); + it('should fill a single Eth2DaiBridge order with a WETH taker fee', async () => { + const order = { + ...eth2DaiBridgeOrder, + takerFee: toBaseUnitAmount(0.01), + }; + await testFactory.marketSellTestAsync([order], 0.78); + }); + it('should fill a single Eth2DaiBridge order with a percentage taker fee', async () => { + const order = { + ...eth2DaiBridgeOrder, + takerFee: toBaseUnitAmount(0.01), + takerFeeAssetData: makerTokenAssetData, + }; + await testFactory.marketSellTestAsync([order], 0.78); + }); + it('should fill an Eth2DaiBridge order along with non-bridge orders, with an affiliate fee', async () => { + const orders = [ + // ERC721 order + await maker.signOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataEncoder + .ERC721Token(erc721Token.address, nftId) + .getABIEncodedTransactionData(), + takerFee: toBaseUnitAmount(0.01), + }), + eth2DaiBridgeOrder, + await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20 + ]; + await testFactory.marketSellTestAsync(orders, 2.56, { forwarderFeePercentage: 1 }); + }); + it('should fully fill a single UniswapBridge order without a taker fee', async () => { + await testFactory.marketSellTestAsync([uniswapBridgeOrder], 1); + }); + it('should partially fill a single UniswapBridge order without a taker fee', async () => { + await testFactory.marketSellTestAsync([uniswapBridgeOrder], 0.34); + }); + it('should correctly handle excess maker asset acquired from Uniswap', async () => { + const bridgeExcessBuyAmount = new BigNumber(1); + await uniswapExchange.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync(); + await testFactory.marketSellTestAsync([uniswapBridgeOrder], 0.34, { bridgeExcessBuyAmount }); + }); + it('should fill a single UniswapBridge order with a WETH taker fee', async () => { + const order = { + ...uniswapBridgeOrder, + takerFee: toBaseUnitAmount(0.01), + }; + await testFactory.marketSellTestAsync([order], 0.78); + }); + it('should fill a single UniswapBridge order with a percentage taker fee', async () => { + const order = { + ...uniswapBridgeOrder, + takerFee: toBaseUnitAmount(0.01), + takerFeeAssetData: makerTokenAssetData, + }; + await testFactory.marketSellTestAsync([order], 0.78); + }); + it('should fill an UniswapBridge order along with non-bridge orders', async () => { + const orders = [ + // ERC721 order + await maker.signOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataEncoder + .ERC721Token(erc721Token.address, nftId) + .getABIEncodedTransactionData(), + takerFee: toBaseUnitAmount(0.01), + }), + uniswapBridgeOrder, + await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20 + ]; + await testFactory.marketSellTestAsync(orders, 2.56, { forwarderFeePercentage: 1 }); + }); + it('should fill multiple bridge orders', async () => { + await testFactory.marketSellTestAsync([eth2DaiBridgeOrder, uniswapBridgeOrder], 1.23); + }); + it('should revert if the takerFee is denominated in a different token', async () => { + const order = { + ...eth2DaiBridgeOrder, + takerFee: toBaseUnitAmount(0.01), + takerFeeAssetData: makerFeeTokenAssetData, + }; + const expectedError = new ForwarderRevertErrors.UnsupportedFeeError(makerFeeTokenAssetData); + await testFactory.marketSellTestAsync([order], 1.23, { revertError: expectedError }); + }); + }); + describe('marketBuyOrdersWithEth', () => { + it('should fully fill a single Eth2DaiBridge order without a taker fee', async () => { + await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 1); + }); + it('should partially fill a single Eth2DaiBridge order without a taker fee', async () => { + await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 0.34); + }); + it('should return excess ETH', async () => { + await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 1, { ethValueAdjustment: 1 }); + }); + it('should correctly handle excess maker asset acquired from Eth2Dai', async () => { + const bridgeExcessBuyAmount = new BigNumber(1); + await eth2Dai.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync(); + await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 0.34, { bridgeExcessBuyAmount }); + }); + it('should fill a single Eth2DaiBridge order with a WETH taker fee', async () => { + const order = { + ...eth2DaiBridgeOrder, + takerFee: toBaseUnitAmount(0.01), + }; + await testFactory.marketBuyTestAsync([order], 0.78); + }); + it('should fill a single Eth2DaiBridge order with a percentage taker fee', async () => { + const order = { + ...eth2DaiBridgeOrder, + takerFee: toBaseUnitAmount(0.01), + takerFeeAssetData: makerTokenAssetData, + }; + await testFactory.marketBuyTestAsync([order], 0.78); + }); + it('should fill an Eth2DaiBridge order along with non-bridge orders, with an affiliate fee', async () => { + const orders = [ + // ERC721 order + await maker.signOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataEncoder + .ERC721Token(erc721Token.address, nftId) + .getABIEncodedTransactionData(), + takerFee: toBaseUnitAmount(0.01), + }), + eth2DaiBridgeOrder, + await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20 + ]; + await testFactory.marketBuyTestAsync(orders, 2.56, { forwarderFeePercentage: 1 }); + }); + it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount (Eth2Dai)', async () => { + const expectedError = new ForwarderRevertErrors.CompleteBuyFailedError( + eth2DaiBridgeOrder.makerAssetAmount.times(0.5), + constants.ZERO_AMOUNT, + ); + await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 0.5, { + ethValueAdjustment: -2, + revertError: expectedError, + }); + }); + it('should fully fill a single UniswapBridge order without a taker fee', async () => { + await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 1); + }); + it('should partially fill a single UniswapBridge order without a taker fee', async () => { + await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 0.34); + }); + it('should correctly handle excess maker asset acquired from Uniswap', async () => { + const bridgeExcessBuyAmount = new BigNumber(1); + await uniswapExchange.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync(); + await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 0.34, { bridgeExcessBuyAmount }); + }); + it('should fill a single UniswapBridge order with a WETH taker fee', async () => { + const order = { + ...uniswapBridgeOrder, + takerFee: toBaseUnitAmount(0.01), + }; + await testFactory.marketBuyTestAsync([order], 0.78); + }); + it('should fill a single UniswapBridge order with a percentage taker fee', async () => { + const order = { + ...uniswapBridgeOrder, + takerFee: toBaseUnitAmount(0.01), + takerFeeAssetData: makerTokenAssetData, + }; + await testFactory.marketBuyTestAsync([order], 0.78); + }); + it('should fill an UniswapBridge order along with non-bridge orders', async () => { + const orders = [ + // ERC721 order + await maker.signOrderAsync({ + makerAssetAmount: new BigNumber(1), + makerAssetData: assetDataEncoder + .ERC721Token(erc721Token.address, nftId) + .getABIEncodedTransactionData(), + takerFee: toBaseUnitAmount(0.01), + }), + uniswapBridgeOrder, + await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20 + ]; + await testFactory.marketBuyTestAsync(orders, 2.56, { forwarderFeePercentage: 1 }); + }); + it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount (Uniswap)', async () => { + const expectedError = new ForwarderRevertErrors.CompleteBuyFailedError( + uniswapBridgeOrder.makerAssetAmount.times(0.5), + constants.ZERO_AMOUNT, + ); + await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 0.5, { + ethValueAdjustment: -2, + revertError: expectedError, + }); + }); + it('should fill multiple bridge orders', async () => { + await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder, uniswapBridgeOrder], 1.23); + }); + it('should revert if the takerFee is denominated in a different token', async () => { + const order = { + ...eth2DaiBridgeOrder, + takerFee: toBaseUnitAmount(0.01), + takerFeeAssetData: makerFeeTokenAssetData, + }; + const expectedError = new ForwarderRevertErrors.UnsupportedFeeError(makerFeeTokenAssetData); + await testFactory.marketBuyTestAsync([order], 1.23, { revertError: expectedError }); + }); + }); +}); +// tslint:disable:max-file-line-count diff --git a/contracts/integrations/test/forwarder/deploy_forwarder.ts b/contracts/integrations/test/forwarder/deploy_forwarder.ts index a2b37df398..9566ab35af 100644 --- a/contracts/integrations/test/forwarder/deploy_forwarder.ts +++ b/contracts/integrations/test/forwarder/deploy_forwarder.ts @@ -17,6 +17,6 @@ export async function deployForwarderAsync( deployment.txDefaults, { ...exchangeArtifacts, ...artifacts }, deployment.exchange.address, - deployment.assetDataEncoder.ERC20Token(deployment.tokens.weth.address).getABIEncodedTransactionData(), + deployment.tokens.weth.address, ); } diff --git a/contracts/integrations/test/forwarder/forwarder_test.ts b/contracts/integrations/test/forwarder/forwarder_test.ts index 438e14c805..d276c74e07 100644 --- a/contracts/integrations/test/forwarder/forwarder_test.ts +++ b/contracts/integrations/test/forwarder/forwarder_test.ts @@ -1,4 +1,3 @@ -import { DevUtilsContract } from '@0x/contracts-dev-utils'; import { DummyERC20TokenContract } from '@0x/contracts-erc20'; import { DummyERC721TokenContract } from '@0x/contracts-erc721'; import { artifacts as exchangeArtifacts, ExchangeContract } from '@0x/contracts-exchange'; @@ -9,7 +8,6 @@ import { expect, getLatestBlockTimestampAsync, getPercentageOfValue, - provider, toBaseUnitAmount, } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; @@ -26,8 +24,6 @@ import { DeploymentManager } from '../framework/deployment_manager'; import { deployForwarderAsync } from './deploy_forwarder'; import { ForwarderTestFactory } from './forwarder_test_factory'; -const devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider); - blockchainTests('Forwarder integration tests', env => { let deployment: DeploymentManager; let forwarder: ForwarderContract; @@ -106,16 +102,7 @@ blockchainTests('Forwarder integration tests', env => { const tokenIds = { erc721: { [erc721Token.address]: [nftId] } }; balanceStore = new BlockchainBalanceStore(tokenOwners, tokenContracts, tokenIds); - testFactory = new ForwarderTestFactory( - forwarder, - deployment, - balanceStore, - maker, - taker, - orderFeeRecipient, - forwarderFeeRecipient, - devUtils, - ); + testFactory = new ForwarderTestFactory(forwarder, deployment, balanceStore, taker, forwarderFeeRecipient); }); after(async () => { @@ -138,7 +125,7 @@ blockchainTests('Forwarder integration tests', env => { env.txDefaults, {}, exchange.address, - wethAssetData, + deployment.tokens.weth.address, ); await expect(deployForwarder).to.revertWith(new ForwarderRevertErrors.UnregisteredAssetProxyError()); }); @@ -202,7 +189,7 @@ blockchainTests('Forwarder integration tests', env => { from: taker.address, }); - const expectedBalances = LocalBalanceStore.create(devUtils, balanceStore); + const expectedBalances = LocalBalanceStore.create(balanceStore); expectedBalances.burnGas(tx.from, DeploymentManager.gasPrice.times(tx.gasUsed)); // Verify balances @@ -521,7 +508,7 @@ blockchainTests('Forwarder integration tests', env => { }); // Compute expected balances - const expectedBalances = LocalBalanceStore.create(devUtils, balanceStore); + const expectedBalances = LocalBalanceStore.create(balanceStore); await expectedBalances.transferAssetAsync( maker.address, taker.address, @@ -578,7 +565,7 @@ blockchainTests('Forwarder integration tests', env => { }); // Compute expected balances - const expectedBalances = LocalBalanceStore.create(devUtils, balanceStore); + const expectedBalances = LocalBalanceStore.create(balanceStore); await expectedBalances.transferAssetAsync( maker.address, taker.address, diff --git a/contracts/integrations/test/forwarder/forwarder_test_factory.ts b/contracts/integrations/test/forwarder/forwarder_test_factory.ts index dfaf8e4d13..7d77641d4f 100644 --- a/contracts/integrations/test/forwarder/forwarder_test_factory.ts +++ b/contracts/integrations/test/forwarder/forwarder_test_factory.ts @@ -1,12 +1,19 @@ -import { DevUtilsContract } from '@0x/contracts-dev-utils'; +import { IAssetDataContract } from '@0x/contracts-asset-proxy'; import { ForwarderContract } from '@0x/contracts-exchange-forwarder'; -import { constants, expect, getPercentageOfValue, OrderStatus } from '@0x/contracts-test-utils'; -import { OrderInfo, SignedOrder } from '@0x/types'; +import { + constants, + expect, + getPercentageOfValue, + hexSlice, + Numberish, + OrderStatus, + provider, +} from '@0x/contracts-test-utils'; +import { AssetProxyId, OrderInfo, SignedOrder } from '@0x/types'; import { BigNumber, RevertError } from '@0x/utils'; import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import { FeeRecipient } from '../framework/actors/fee_recipient'; -import { Maker } from '../framework/actors/maker'; import { Taker } from '../framework/actors/taker'; import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store'; import { LocalBalanceStore } from '../framework/balances/local_balance_store'; @@ -20,24 +27,44 @@ interface ForwarderFillState { } interface MarketSellOptions { - forwarderFeePercentage: BigNumber; + forwarderFeePercentage: Numberish; revertError: RevertError; + bridgeExcessBuyAmount: BigNumber; } interface MarketBuyOptions extends MarketSellOptions { ethValueAdjustment: number; // Used to provided insufficient/excess ETH } +function areUnderlyingAssetsEqual(assetData1: string, assetData2: string): boolean { + const assetProxyId1 = hexSlice(assetData1, 0, 4); + const assetProxyId2 = hexSlice(assetData2, 0, 4); + if ( + (assetProxyId1 === AssetProxyId.ERC20 || assetProxyId1 === AssetProxyId.ERC20Bridge) && + (assetProxyId2 === AssetProxyId.ERC20 || assetProxyId2 === AssetProxyId.ERC20Bridge) + ) { + const assetDataDecoder = new IAssetDataContract(constants.NULL_ADDRESS, provider); + const tokenAddress1 = + assetProxyId1 === AssetProxyId.ERC20 + ? assetDataDecoder.getABIDecodedTransactionData('ERC20Token', assetData1) + : assetDataDecoder.getABIDecodedTransactionData<[string]>('ERC20Bridge', assetData1)[0]; + const tokenAddress2 = + assetProxyId2 === AssetProxyId.ERC20 + ? assetDataDecoder.getABIDecodedTransactionData('ERC20Token', assetData2) + : assetDataDecoder.getABIDecodedTransactionData<[string]>('ERC20Bridge', assetData2)[0]; + return tokenAddress2 === tokenAddress1; + } else { + return false; + } +} + export class ForwarderTestFactory { constructor( private readonly _forwarder: ForwarderContract, private readonly _deployment: DeploymentManager, private readonly _balanceStore: BlockchainBalanceStore, - private readonly _maker: Maker, private readonly _taker: Taker, - private readonly _orderFeeRecipient: FeeRecipient, private readonly _forwarderFeeRecipient: FeeRecipient, - private readonly _devUtils: DevUtilsContract, ) {} public async marketBuyTestAsync( @@ -69,7 +96,7 @@ export class ForwarderTestFactory { const tx = this._forwarder .marketBuyOrdersWithEth( orders, - makerAssetAcquiredAmount, + makerAssetAcquiredAmount.minus(options.bridgeExcessBuyAmount || 0), orders.map(signedOrder => signedOrder.signature), feePercentage, this._forwarderFeeRecipient.address, @@ -164,7 +191,7 @@ export class ForwarderTestFactory { options: Partial, ): Promise { await this._balanceStore.updateBalancesAsync(); - const balances = LocalBalanceStore.create(this._devUtils, this._balanceStore); + const balances = LocalBalanceStore.create(this._balanceStore); const currentTotal = { wethSpentAmount: constants.ZERO_AMOUNT, makerAssetAcquiredAmount: constants.ZERO_AMOUNT, @@ -185,6 +212,7 @@ export class ForwarderTestFactory { order, ordersInfoBefore[i].orderTakerAssetFilledAmount, Math.min(remainingOrdersToFill, 1), + options.bridgeExcessBuyAmount || constants.ZERO_AMOUNT, ); remainingOrdersToFill = Math.max(remainingOrdersToFill - 1, 0); @@ -209,6 +237,7 @@ export class ForwarderTestFactory { order: SignedOrder, takerAssetFilled: BigNumber, fillFraction: number, + bridgeExcessBuyAmount: BigNumber, ): Promise { let { makerAssetAmount, takerAssetAmount, makerFee, takerFee } = order; makerAssetAmount = makerAssetAmount.times(fillFraction).integerValue(BigNumber.ROUND_CEIL); @@ -228,9 +257,10 @@ export class ForwarderTestFactory { const takerFeeFilled = takerAssetFilled.times(order.takerFee).dividedToIntegerBy(order.takerAssetAmount); takerFee = BigNumber.max(takerFee.minus(takerFeeFilled), 0); + makerAssetAmount = makerAssetAmount.plus(bridgeExcessBuyAmount); let wethSpentAmount = takerAssetAmount.plus(DeploymentManager.protocolFee); let makerAssetAcquiredAmount = makerAssetAmount; - if (order.takerFeeAssetData === order.makerAssetData) { + if (areUnderlyingAssetsEqual(order.takerFeeAssetData, order.makerAssetData)) { makerAssetAcquiredAmount = makerAssetAcquiredAmount.minus(takerFee); } else if (order.takerFeeAssetData === order.takerAssetData) { wethSpentAmount = wethSpentAmount.plus(takerFee); @@ -244,29 +274,29 @@ export class ForwarderTestFactory { // Maker -> Forwarder await balances.transferAssetAsync( - this._maker.address, + order.makerAddress, this._forwarder.address, makerAssetAmount, order.makerAssetData, ); // Maker -> Order fee recipient await balances.transferAssetAsync( - this._maker.address, - this._orderFeeRecipient.address, + order.makerAddress, + order.feeRecipientAddress, makerFee, order.makerFeeAssetData, ); // Forwarder -> Maker await balances.transferAssetAsync( this._forwarder.address, - this._maker.address, + order.makerAddress, takerAssetAmount, order.takerAssetData, ); // Forwarder -> Order fee recipient await balances.transferAssetAsync( this._forwarder.address, - this._orderFeeRecipient.address, + order.feeRecipientAddress, takerFee, order.takerFeeAssetData, ); diff --git a/contracts/integrations/test/framework/assertions/stake.ts b/contracts/integrations/test/framework/assertions/stake.ts index 5954a711ce..9783a97092 100644 --- a/contracts/integrations/test/framework/assertions/stake.ts +++ b/contracts/integrations/test/framework/assertions/stake.ts @@ -36,7 +36,7 @@ export function validStakeAssertion( return new FunctionAssertion(stakingWrapper.stake, { before: async (amount: BigNumber, txData: Partial) => { // Simulates the transfer of ZRX from staker to vault - const expectedBalances = LocalBalanceStore.create(deployment.devUtils, balanceStore); + const expectedBalances = LocalBalanceStore.create(balanceStore); await expectedBalances.transferAssetAsync( txData.from as string, zrxVault.address, diff --git a/contracts/integrations/test/framework/assertions/unstake.ts b/contracts/integrations/test/framework/assertions/unstake.ts index 9c854eb18d..5c3e26c193 100644 --- a/contracts/integrations/test/framework/assertions/unstake.ts +++ b/contracts/integrations/test/framework/assertions/unstake.ts @@ -36,7 +36,7 @@ export function validUnstakeAssertion( return new FunctionAssertion(stakingWrapper.unstake, { before: async (amount: BigNumber, txData: Partial) => { // Simulates the transfer of ZRX from vault to staker - const expectedBalances = LocalBalanceStore.create(deployment.devUtils, balanceStore); + const expectedBalances = LocalBalanceStore.create(balanceStore); await expectedBalances.transferAssetAsync( zrxVault.address, txData.from as string, diff --git a/contracts/integrations/test/framework/balances/local_balance_store.ts b/contracts/integrations/test/framework/balances/local_balance_store.ts index ed2a357e07..235e418f72 100644 --- a/contracts/integrations/test/framework/balances/local_balance_store.ts +++ b/contracts/integrations/test/framework/balances/local_balance_store.ts @@ -1,5 +1,5 @@ -import { DevUtilsContract } from '@0x/contracts-dev-utils'; -import { constants, Numberish } from '@0x/contracts-test-utils'; +import { IAssetDataContract } from '@0x/contracts-asset-proxy'; +import { constants, hexSlice, Numberish, provider } from '@0x/contracts-test-utils'; import { AssetProxyId } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; @@ -8,12 +8,14 @@ import { BalanceStore } from './balance_store'; import { TokenContractsByName, TokenOwnersByName } from './types'; export class LocalBalanceStore extends BalanceStore { + private readonly _assetDataDecoder: IAssetDataContract; + /** * Creates a new balance store based on an existing one. * @param sourceBalanceStore Existing balance store whose values should be copied. */ - public static create(devUtils: DevUtilsContract, sourceBalanceStore?: BalanceStore): LocalBalanceStore { - const localBalanceStore = new LocalBalanceStore(devUtils); + public static create(sourceBalanceStore?: BalanceStore): LocalBalanceStore { + const localBalanceStore = new LocalBalanceStore(); if (sourceBalanceStore !== undefined) { localBalanceStore.cloneFrom(sourceBalanceStore); } @@ -26,11 +28,11 @@ export class LocalBalanceStore extends BalanceStore { * be initialized via `create`. */ protected constructor( - private readonly _devUtils: DevUtilsContract, tokenOwnersByName: TokenOwnersByName = {}, tokenContractsByName: Partial = {}, ) { super(tokenOwnersByName, tokenContractsByName); + this._assetDataDecoder = new IAssetDataContract(constants.NULL_ADDRESS, provider); } /** @@ -78,25 +80,41 @@ export class LocalBalanceStore extends BalanceStore { amount: BigNumber, assetData: string, ): Promise { - if (fromAddress === toAddress) { + if (fromAddress === toAddress || amount.isZero()) { return; } - const assetProxyId = await this._devUtils.decodeAssetProxyId(assetData).callAsync(); + const assetProxyId = hexSlice(assetData, 0, 4); switch (assetProxyId) { case AssetProxyId.ERC20: { - // tslint:disable-next-line:no-unused-variable - const [_proxyId, tokenAddress] = await this._devUtils.decodeERC20AssetData(assetData).callAsync(); + const tokenAddress = this._assetDataDecoder.getABIDecodedTransactionData( + 'ERC20Token', + assetData, + ); _.update(this.balances.erc20, [fromAddress, tokenAddress], balance => balance.minus(amount)); _.update(this.balances.erc20, [toAddress, tokenAddress], balance => (balance || constants.ZERO_AMOUNT).plus(amount), ); break; } + case AssetProxyId.ERC20Bridge: { + const [tokenAddress] = this._assetDataDecoder.getABIDecodedTransactionData<[string]>( + 'ERC20Bridge', + assetData, + ); + // The test bridge contract (TestEth2DaiBridge or TestUniswapBridge) will be the + // fromAddress in this case, and it simply mints the amount of token it needs to transfer. + _.update(this.balances.erc20, [fromAddress, tokenAddress], balance => + (balance || constants.ZERO_AMOUNT).minus(amount), + ); + _.update(this.balances.erc20, [toAddress, tokenAddress], balance => + (balance || constants.ZERO_AMOUNT).plus(amount), + ); + break; + } case AssetProxyId.ERC721: { - // tslint:disable-next-line:no-unused-variable - const [_proxyId, tokenAddress, tokenId] = await this._devUtils - .decodeERC721AssetData(assetData) - .callAsync(); + const [tokenAddress, tokenId] = this._assetDataDecoder.getABIDecodedTransactionData< + [string, BigNumber] + >('ERC721Token', assetData); const fromTokens = _.get(this.balances.erc721, [fromAddress, tokenAddress], []); const toTokens = _.get(this.balances.erc721, [toAddress, tokenAddress], []); if (amount.gte(1)) { @@ -112,12 +130,9 @@ export class LocalBalanceStore extends BalanceStore { break; } case AssetProxyId.ERC1155: { - const [ - _proxyId, // tslint:disable-line:no-unused-variable - tokenAddress, - tokenIds, - tokenValues, - ] = await this._devUtils.decodeERC1155AssetData(assetData).callAsync(); + const [tokenAddress, tokenIds, tokenValues] = this._assetDataDecoder.getABIDecodedTransactionData< + [string, BigNumber[], BigNumber[]] + >('ERC1155Assets', assetData); const fromBalances = { // tslint:disable-next-line:no-inferred-empty-object-type fungible: _.get(this.balances.erc1155, [fromAddress, tokenAddress, 'fungible'], {}), @@ -154,10 +169,9 @@ export class LocalBalanceStore extends BalanceStore { break; } case AssetProxyId.MultiAsset: { - // tslint:disable-next-line:no-unused-variable - const [_proxyId, amounts, nestedAssetData] = await this._devUtils - .decodeMultiAssetData(assetData) - .callAsync(); + const [amounts, nestedAssetData] = this._assetDataDecoder.getABIDecodedTransactionData< + [BigNumber[], string[]] + >('MultiAsset', assetData); for (const [i, amt] of amounts.entries()) { const nestedAmount = amount.times(amt); await this.transferAssetAsync(fromAddress, toAddress, nestedAmount, nestedAssetData[i]); diff --git a/contracts/integrations/test/framework/deployment_manager.ts b/contracts/integrations/test/framework/deployment_manager.ts index e67074d1c0..f1d3a2b87a 100644 --- a/contracts/integrations/test/framework/deployment_manager.ts +++ b/contracts/integrations/test/framework/deployment_manager.ts @@ -1,6 +1,7 @@ import { artifacts as assetProxyArtifacts, ERC1155ProxyContract, + ERC20BridgeProxyContract, ERC20ProxyContract, ERC721ProxyContract, IAssetDataContract, @@ -85,6 +86,7 @@ interface AssetProxyContracts { erc1155Proxy: ERC1155ProxyContract; multiAssetProxy: MultiAssetProxyContract; staticCallProxy: StaticCallProxyContract; + erc20BridgeProxy: ERC20BridgeProxyContract; } // Contract wrappers for all of the staking contracts @@ -189,6 +191,7 @@ export class DeploymentManager { assetProxies.erc721Proxy, assetProxies.erc1155Proxy, assetProxies.multiAssetProxy, + assetProxies.erc20BridgeProxy, exchange, staking.stakingProxy, ]); @@ -232,6 +235,7 @@ export class DeploymentManager { assetProxies.erc1155Proxy.address, assetProxies.multiAssetProxy.address, assetProxies.staticCallProxy.address, + assetProxies.erc20BridgeProxy.address, ], ); @@ -244,13 +248,19 @@ export class DeploymentManager { assetProxies.erc721Proxy.address, assetProxies.erc1155Proxy.address, assetProxies.staticCallProxy.address, + assetProxies.erc20BridgeProxy.address, ], ); // Add the multi-asset proxy as an authorized address of the token proxies. await batchAddAuthorizedAddressAsync( owner, - [assetProxies.erc20Proxy, assetProxies.erc721Proxy, assetProxies.erc1155Proxy], + [ + assetProxies.erc20Proxy, + assetProxies.erc721Proxy, + assetProxies.erc1155Proxy, + assetProxies.erc20BridgeProxy, + ], [assetProxies.multiAssetProxy.address], ); @@ -262,6 +272,7 @@ export class DeploymentManager { assetProxies.erc721Proxy, assetProxies.erc1155Proxy, assetProxies.multiAssetProxy, + assetProxies.erc20BridgeProxy, ], [exchange.address], ); @@ -327,12 +338,19 @@ export class DeploymentManager { txDefaults, assetProxyArtifacts, ); + const erc20BridgeProxy = await ERC20BridgeProxyContract.deployFrom0xArtifactAsync( + assetProxyArtifacts.ERC20BridgeProxy, + environment.provider, + txDefaults, + assetProxyArtifacts, + ); return { erc20Proxy, erc721Proxy, erc1155Proxy, multiAssetProxy, staticCallProxy, + erc20BridgeProxy, }; } diff --git a/contracts/integrations/test/wrappers.ts b/contracts/integrations/test/wrappers.ts index 74a642fc95..5effb14bb4 100644 --- a/contracts/integrations/test/wrappers.ts +++ b/contracts/integrations/test/wrappers.ts @@ -3,4 +3,9 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ +export * from '../test/generated-wrappers/test_eth2_dai'; +export * from '../test/generated-wrappers/test_eth2_dai_bridge'; export * from '../test/generated-wrappers/test_framework'; +export * from '../test/generated-wrappers/test_uniswap_bridge'; +export * from '../test/generated-wrappers/test_uniswap_exchange'; +export * from '../test/generated-wrappers/test_uniswap_exchange_factory'; diff --git a/contracts/integrations/tsconfig.json b/contracts/integrations/tsconfig.json index 18b5c60dac..0ad453da1c 100644 --- a/contracts/integrations/tsconfig.json +++ b/contracts/integrations/tsconfig.json @@ -2,5 +2,13 @@ "extends": "../../tsconfig", "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], - "files": ["generated-artifacts/TestFramework.json", "test/generated-artifacts/TestFramework.json"] + "files": [ + "generated-artifacts/TestFramework.json", + "test/generated-artifacts/TestEth2Dai.json", + "test/generated-artifacts/TestEth2DaiBridge.json", + "test/generated-artifacts/TestFramework.json", + "test/generated-artifacts/TestUniswapBridge.json", + "test/generated-artifacts/TestUniswapExchange.json", + "test/generated-artifacts/TestUniswapExchangeFactory.json" + ] }