diff --git a/contracts/integrations/CHANGELOG.json b/contracts/integrations/CHANGELOG.json index 7689e01556..024c43baf5 100644 --- a/contracts/integrations/CHANGELOG.json +++ b/contracts/integrations/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Integration tests for DydxBridge with (i) Exchange v3 and (ii) Mainnet dYdX SoloMargin contract.", "pr": 2401 + }, + { + "note": "Add aggregator mainnet tests.", + "pr": 2407 } ] }, diff --git a/contracts/integrations/contracts/test/TestMainnetAggregatorFills.sol b/contracts/integrations/contracts/test/TestMainnetAggregatorFills.sol new file mode 100644 index 0000000000..facd42997d --- /dev/null +++ b/contracts/integrations/contracts/test/TestMainnetAggregatorFills.sol @@ -0,0 +1,180 @@ +/* + + 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-erc20/contracts/src/interfaces/IERC20Token.sol"; +import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; +import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; +import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol"; +import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; +import "@0x/contracts-exchange-libs/contracts/src/LibFillResults.sol"; +import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; +import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; + + +/// @dev A forwarder contract for filling 0x asset-swapper aggregated orders. +/// The forwarder is necessary to purchase taker assets and set up +/// approvals in one transaction. Only call the functions on this contract +/// in an `eth_call` context or you will lose money! +contract TestMainnetAggregatorFills is + DeploymentConstants +{ + using LibSafeMath for uint256; + + address constant internal EXCHANGE_ADDRESS = 0x61935CbDd02287B511119DDb11Aeb42F1593b7Ef; + bytes4 constant internal ERC20_PROXY_ID = 0xf47261b0; // bytes4(keccak256("ERC20Token(address)")); + + struct SimulatedMarketFillResults { + uint256 makerAssetBalanceBefore; + uint256 takerAssetBalanceBefore; + uint256 makerAssetBalanceAfter; + uint256 takerAssetBalanceAfter; + LibFillResults.FillResults fillResults; + } + + // solhint-disable-next-line no-empty-blocks + function() external payable {} + + /// @dev Buy taker assets with ETH from `takerOrders` and then perform a + /// market buy on `makerOrders`. + function marketBuy( + address makerTokenAddress, + address takerTokenAddress, + LibOrder.Order[] memory makerOrders, + LibOrder.Order[] memory takerOrders, + bytes[] memory makerOrderSignatures, + bytes[] memory takerOrderSignatures, + uint256 makerAssetBuyAmount + ) + public + payable + returns (SimulatedMarketFillResults memory results) + { + _prepareFunds(takerTokenAddress, makerOrders, takerOrders, takerOrderSignatures); + results.makerAssetBalanceBefore = IERC20Token(makerTokenAddress).balanceOf(address(this)); + results.takerAssetBalanceBefore = IERC20Token(takerTokenAddress).balanceOf(address(this)); + results.fillResults = IExchange(EXCHANGE_ADDRESS) + .marketBuyOrdersNoThrow + .value(address(this).balance)( + makerOrders, + makerAssetBuyAmount, + makerOrderSignatures + ); + results.makerAssetBalanceAfter = IERC20Token(makerTokenAddress).balanceOf(address(this)); + results.takerAssetBalanceAfter = IERC20Token(takerTokenAddress).balanceOf(address(this)); + } + + /// @dev Buy taker assets with ETH from `takerOrders` and then perform a + /// market sell on `makerOrders`. + function marketSell( + address makerTokenAddress, + address takerTokenAddress, + LibOrder.Order[] memory makerOrders, + LibOrder.Order[] memory takerOrders, + bytes[] memory makerOrderSignatures, + bytes[] memory takerOrderSignatures, + uint256 takerAssetSellAmount + ) + public + payable + returns (SimulatedMarketFillResults memory results) + { + _prepareFunds(takerTokenAddress, makerOrders, takerOrders, takerOrderSignatures); + results.makerAssetBalanceBefore = IERC20Token(makerTokenAddress).balanceOf(address(this)); + results.takerAssetBalanceBefore = IERC20Token(takerTokenAddress).balanceOf(address(this)); + results.fillResults = IExchange(EXCHANGE_ADDRESS) + .marketSellOrdersNoThrow + .value(address(this).balance)( + makerOrders, + takerAssetSellAmount, + makerOrderSignatures + ); + results.makerAssetBalanceAfter = IERC20Token(makerTokenAddress).balanceOf(address(this)); + results.takerAssetBalanceAfter = IERC20Token(takerTokenAddress).balanceOf(address(this)); + } + + /// @dev Like `marketSell`, but calls `fillOrder()` individually to detect + /// errors. + function fillOrders( + address makerTokenAddress, + address takerTokenAddress, + LibOrder.Order[] memory makerOrders, + LibOrder.Order[] memory takerOrders, + bytes[] memory makerOrderSignatures, + bytes[] memory takerOrderSignatures, + uint256 takerAssetSellAmount + ) + public + payable + returns (SimulatedMarketFillResults memory results) + { + _prepareFunds(takerTokenAddress, makerOrders, takerOrders, takerOrderSignatures); + results.makerAssetBalanceBefore = IERC20Token(makerTokenAddress).balanceOf(address(this)); + results.takerAssetBalanceBefore = IERC20Token(takerTokenAddress).balanceOf(address(this)); + for (uint256 i = 0; i < makerOrders.length; i++) { + if (takerAssetSellAmount == 0) { + break; + } + LibFillResults.FillResults memory fillResults = IExchange(EXCHANGE_ADDRESS) + .fillOrder + .value(address(this).balance)( + makerOrders[i], + takerAssetSellAmount, + makerOrderSignatures[i] + ); + results.fillResults = LibFillResults.addFillResults(results.fillResults, fillResults); + takerAssetSellAmount = takerAssetSellAmount.safeSub(fillResults.takerAssetFilledAmount); + } + results.makerAssetBalanceAfter = IERC20Token(makerTokenAddress).balanceOf(address(this)); + results.takerAssetBalanceAfter = IERC20Token(takerTokenAddress).balanceOf(address(this)); + } + + function _approveAssetProxy(address tokenAddress) private { + address assetProxyAddress = IExchange(EXCHANGE_ADDRESS).getAssetProxy(ERC20_PROXY_ID); + LibERC20Token.approve(tokenAddress, assetProxyAddress, uint256(-1)); + } + + /// @dev Buys as much of `takerOrders` as possible with the ETH transferred + /// to this contract, leaving enough ETH behind for protocol fees. + function _prepareFunds( + address takerTokenAddress, + LibOrder.Order[] memory makerOrders, + LibOrder.Order[] memory takerOrders, + bytes[] memory takerOrderSignatures + ) + private + { + _approveAssetProxy(_getWethAddress()); + uint256 protocolFee = IExchange(EXCHANGE_ADDRESS).protocolFeeMultiplier() * tx.gasprice; + uint256 maxProtocolFees = protocolFee * (takerOrders.length + makerOrders.length); + uint256 ethSellAmount = msg.value.safeSub(maxProtocolFees); + IEtherToken(_getWethAddress()).deposit.value(ethSellAmount)(); + if (takerTokenAddress != _getWethAddress()) { + IExchange(EXCHANGE_ADDRESS) + .marketSellOrdersNoThrow + .value(maxProtocolFees)( + takerOrders, + ethSellAmount, + takerOrderSignatures + ); + _approveAssetProxy(takerTokenAddress); + } + } +} diff --git a/contracts/integrations/package.json b/contracts/integrations/package.json index 800065e5cd..e2d098b7ab 100644 --- a/contracts/integrations/package.json +++ b/contracts/integrations/package.json @@ -38,7 +38,7 @@ }, "config": { "publicInterfaceContracts": "TestFramework", - "abis": "./test/generated-artifacts/@(TestDydxUser|TestEth2Dai|TestEth2DaiBridge|TestFramework|TestUniswapBridge|TestUniswapExchange|TestUniswapExchangeFactory).json", + "abis": "./test/generated-artifacts/@(TestDydxUser|TestEth2Dai|TestEth2DaiBridge|TestFramework|TestMainnetAggregatorFills|TestUniswapBridge|TestUniswapExchange|TestUniswapExchangeFactory).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { @@ -89,6 +89,7 @@ "typescript": "3.0.1" }, "dependencies": { + "@0x/asset-swapper": "^3.0.1", "@0x/base-contract": "^6.0.2", "@0x/contracts-asset-proxy": "^3.0.2", "@0x/contracts-erc1155": "^2.0.2", diff --git a/contracts/integrations/test/aggregation/fill_test.ts b/contracts/integrations/test/aggregation/fill_test.ts new file mode 100644 index 0000000000..14c1eef3bc --- /dev/null +++ b/contracts/integrations/test/aggregation/fill_test.ts @@ -0,0 +1,239 @@ +import { MarketBuySwapQuote, MarketSellSwapQuote, Orderbook, SwapQuoter } from '@0x/asset-swapper'; +import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { FillResults, SignedOrder } from '@0x/types'; +import { BigNumber, logUtils } from '@0x/utils'; +import * as _ from 'lodash'; + +import { TestMainnetAggregatorFillsContract } from '../wrappers'; + +import { tokens } from './tokens'; + +blockchainTests.live('Aggregator Mainnet Tests', env => { + // Mainnet address of the `TestMainnetAggregatorFills` contract. + const TEST_CONTRACT_ADDRESS = '0x37Ca306F42748b7fe105F89FCBb2CD03D27c8146'; + const TAKER_ADDRESS = '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B'; // Vitalik + const ORDERBOOK_POLLING_MS = 1000; + const GAS_PRICE = new BigNumber(1); + const TAKER_ASSET_ETH_VALUE = 500e18; + const MIN_BALANCE = 500.1e18; + const SYMBOLS = ['ETH', 'DAI', 'USDC', 'FOAM']; + const TEST_PAIRS = _.flatten(SYMBOLS.map(m => SYMBOLS.filter(t => t !== m).map(t => [m, t]))); + const FILL_VALUES = [1, 10, 1e2, 1e3, 1e4, 2.5e4, 5e4]; + + let testContract: TestMainnetAggregatorFillsContract; + let swapQuoter: SwapQuoter; + let takerEthBalance: BigNumber; + const orderbooks: { [name: string]: Orderbook } = {}; + + async function getTakerOrdersAsync(takerAssetSymbol: string): Promise { + if (takerAssetSymbol === 'ETH') { + return []; + } + return getOrdersAsync(takerAssetSymbol, 'ETH'); + } + + // Fetches ETH -> taker asset orders for the forwarder contract. + async function getOrdersAsync(makerAssetSymbol: string, takerAssetSymbol: string): Promise { + const takerTokenAddress = tokens[takerAssetSymbol].address; + const makerTokenAddress = tokens[makerAssetSymbol].address; + const makerAssetData = assetDataUtils.encodeERC20AssetData(makerTokenAddress); + const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress); + const orders = _.flatten( + await Promise.all( + Object.keys(orderbooks).map(async name => + getOrdersFromOrderBookAsync(name, makerAssetData, takerAssetData), + ), + ), + ); + const uniqueOrders: SignedOrder[] = []; + for (const order of orders) { + if (!order.makerFee.eq(0) || !order.takerFee.eq(0)) { + continue; + } + if (uniqueOrders.findIndex(o => isSameOrder(order, o)) === -1) { + uniqueOrders.push(order); + } + } + return uniqueOrders; + } + + async function getOrdersFromOrderBookAsync( + name: string, + makerAssetData: string, + takerAssetData: string, + ): Promise { + try { + return (await orderbooks[name].getOrdersAsync(makerAssetData, takerAssetData)).map(r => r.order); + } catch (err) { + logUtils.warn(`Failed to retrieve orders from orderbook "${name}".`); + } + return []; + } + + function isSameOrder(a: SignedOrder, b: SignedOrder): boolean { + for (const [k, v] of Object.entries(a)) { + if (k in (b as any)) { + if (BigNumber.isBigNumber(v) && !v.eq((b as any)[k])) { + return false; + } + if (v !== (b as any)[k]) { + return false; + } + } + } + return true; + } + + function toTokenUnits(symbol: string, weis: Numberish): BigNumber { + return new BigNumber(weis).div(new BigNumber(10).pow(tokens[symbol].decimals)); + } + + function fromTokenUnits(symbol: string, units: Numberish): BigNumber { + return new BigNumber(units) + .times(new BigNumber(10).pow(tokens[symbol].decimals)) + .integerValue(BigNumber.ROUND_DOWN); + } + + interface MarketOperationResult { + makerAssetBalanceBefore: BigNumber; + takerAssetBalanceBefore: BigNumber; + makerAssetBalanceAfter: BigNumber; + takerAssetBalanceAfter: BigNumber; + fillResults: FillResults; + } + + // Liquidity is low right now so it's possible we didn't have + // enough taker assets to cover the orders, so occasionally we'll get incomplete + // fills. This function will catch those cases. + // TODO(dorothy-zbornak): Remove this special case when liquidity is up. + function checkHadEnoughTakerAsset( + quote: MarketBuySwapQuote | MarketSellSwapQuote, + result: MarketOperationResult, + ): boolean { + if (result.takerAssetBalanceBefore.gte(quote.worstCaseQuoteInfo.takerAssetAmount)) { + return true; + } + const takerAssetPct = result.takerAssetBalanceBefore + .div(quote.worstCaseQuoteInfo.takerAssetAmount) + .times(100) + .toNumber() + .toFixed(1); + logUtils.warn(`Could not acquire enough taker asset to complete the fill: ${takerAssetPct}%`); + expect(result.fillResults.makerAssetFilledAmount).to.bignumber.lt(quote.worstCaseQuoteInfo.makerAssetAmount); + return false; + } + + before(async () => { + testContract = new TestMainnetAggregatorFillsContract(TEST_CONTRACT_ADDRESS, env.provider, { + ...env.txDefaults, + gasPrice: GAS_PRICE, + gas: 10e6, + }); + swapQuoter = SwapQuoter.getSwapQuoterForStandardRelayerAPIUrl(env.provider, 'https://api.0x.org/sra'); + // Pool orderbooks because we're desperate for liquidity. + orderbooks.swapQuoter = swapQuoter.orderbook; + orderbooks.bamboo = Orderbook.getOrderbookForPollingProvider({ + httpEndpoint: 'https://sra.bamboorelay.com/0x/v3', + pollingIntervalMs: ORDERBOOK_POLLING_MS, + }); + // TODO(dorothy-zbornak): Uncomment when radar's SRA is up. + // orderbooks.radar = Orderbook.getOrderbookForPollingProvider({ + // httpEndpoint: 'https://api-v3.radarrelay.com/v3', + // pollingIntervalMs: ORDERBOOK_POLLING_MS, + // }); + takerEthBalance = await env.web3Wrapper.getBalanceInWeiAsync(TAKER_ADDRESS); + }); + + it('taker has minimum ETH', async () => { + expect(takerEthBalance).to.bignumber.gte(MIN_BALANCE); + }); + + describe('market sells', () => { + for (const [makerSymbol, takerSymbol] of TEST_PAIRS) { + for (const fillValue of FILL_VALUES) { + const fillAmount = fromTokenUnits(takerSymbol, new BigNumber(fillValue).div(tokens[takerSymbol].price)); + it(`sell ${toTokenUnits(takerSymbol, fillAmount)} ${takerSymbol} for ${makerSymbol}`, async () => { + const [quote, takerOrders] = await Promise.all([ + swapQuoter.getMarketSellSwapQuoteAsync( + tokens[makerSymbol].address, + tokens[takerSymbol].address, + fillAmount, + { gasPrice: GAS_PRICE }, + ), + getTakerOrdersAsync(takerSymbol), + ]); + // Buy taker assets from `takerOrders` and and perform a + // market sell on the bridge orders. + const fill = await testContract + .marketSell( + tokens[makerSymbol].address, + tokens[takerSymbol].address, + quote.orders, + takerOrders, + quote.orders.map(o => o.signature), + takerOrders.map(o => o.signature), + quote.takerAssetFillAmount, + ) + .callAsync({ + value: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount.plus(TAKER_ASSET_ETH_VALUE), + from: TAKER_ADDRESS, + gasPrice: quote.gasPrice, + }); + if (checkHadEnoughTakerAsset(quote, fill)) { + expect(fill.fillResults.makerAssetFilledAmount, 'makerAssetFilledAmount').to.bignumber.gte( + quote.worstCaseQuoteInfo.makerAssetAmount, + ); + expect(fill.fillResults.takerAssetFilledAmount, 'takerAssetFilledAmount').to.bignumber.lte( + quote.takerAssetFillAmount, + ); + } + }); + } + } + }); + + describe('market buys', () => { + for (const [makerSymbol, takerSymbol] of TEST_PAIRS) { + for (const fillValue of FILL_VALUES) { + const fillAmount = fromTokenUnits(makerSymbol, new BigNumber(fillValue).div(tokens[makerSymbol].price)); + it(`buy ${toTokenUnits(makerSymbol, fillAmount)} ${makerSymbol} with ${takerSymbol}`, async () => { + const [quote, takerOrders] = await Promise.all([ + swapQuoter.getMarketBuySwapQuoteAsync( + tokens[makerSymbol].address, + tokens[takerSymbol].address, + fillAmount, + { gasPrice: GAS_PRICE }, + ), + getTakerOrdersAsync(takerSymbol), + ]); + // Buy taker assets from `takerOrders` and and perform a + // market buy on the bridge orders. + const fill = await testContract + .marketBuy( + tokens[makerSymbol].address, + tokens[takerSymbol].address, + quote.orders, + takerOrders, + quote.orders.map(o => o.signature), + takerOrders.map(o => o.signature), + quote.makerAssetFillAmount, + ) + .callAsync({ + value: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount.plus(TAKER_ASSET_ETH_VALUE), + from: TAKER_ADDRESS, + gasPrice: quote.gasPrice, + }); + if (checkHadEnoughTakerAsset(quote, fill)) { + expect(fill.fillResults.takerAssetFilledAmount, 'takerAssetFilledAmount').to.bignumber.lte( + quote.worstCaseQuoteInfo.takerAssetAmount, + ); + expect(fill.fillResults.makerAssetFilledAmount, 'makerAssetFilledAmount').to.bignumber.gte( + quote.makerAssetFillAmount, + ); + } + }); + } + } + }); +}); diff --git a/contracts/integrations/test/aggregation/tokens.ts b/contracts/integrations/test/aggregation/tokens.ts new file mode 100644 index 0000000000..5fc3f29e2a --- /dev/null +++ b/contracts/integrations/test/aggregation/tokens.ts @@ -0,0 +1,77 @@ +export const tokens: { [symbol: string]: { address: string; decimals: number; price: number } } = { + ETH: { + address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + decimals: 18, + price: 133, + }, + SAI: { + address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', + decimals: 18, + price: 1, + }, + DAI: { + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + decimals: 18, + price: 1, + }, + USDC: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + decimals: 6, + price: 1, + }, + WBTC: { + address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', + decimals: 8, + price: 6900, + }, + MKR: { + address: '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', + decimals: 18, + price: 454, + }, + BAT: { + address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', + decimals: 18, + price: 0.17, + }, + OMG: { + address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', + decimals: 18, + price: 0.65, + }, + ZRX: { + address: '0xE41d2489571d322189246DaFA5ebDe1F4699F498', + decimals: 18, + price: 0.19, + }, + ZIL: { + address: '0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27', + decimals: 12, + price: 0.004, + }, + FOAM: { + address: '0x4946Fcea7C692606e8908002e55A582af44AC121', + decimals: 18, + price: 0.004, + }, + USDT: { + address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + decimals: 6, + price: 0.019, + }, + REP: { + address: '0x1985365e9f78359a9B6AD760e32412f4a445E862', + decimals: 18, + price: 8.9, + }, + MANA: { + address: '0x0F5D2fB29fb7d3CFeE444a200298f468908cC942', + decimals: 18, + price: 0.025, + }, + LINK: { + address: '0x514910771AF9Ca656af840dff83E8264EcF986CA', + decimals: 18, + price: 1.8, + }, +}; diff --git a/contracts/integrations/test/artifacts.ts b/contracts/integrations/test/artifacts.ts index 005abcab10..3673b475fe 100644 --- a/contracts/integrations/test/artifacts.ts +++ b/contracts/integrations/test/artifacts.ts @@ -9,6 +9,7 @@ import * as TestDydxUser from '../test/generated-artifacts/TestDydxUser.json'; 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'; +import * as TestMainnetAggregatorFills from '../test/generated-artifacts/TestMainnetAggregatorFills.json'; 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'; @@ -17,6 +18,7 @@ export const artifacts = { TestEth2Dai: TestEth2Dai as ContractArtifact, TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, TestFramework: TestFramework as ContractArtifact, + TestMainnetAggregatorFills: TestMainnetAggregatorFills as ContractArtifact, TestUniswapBridge: TestUniswapBridge as ContractArtifact, TestUniswapExchange: TestUniswapExchange as ContractArtifact, TestUniswapExchangeFactory: TestUniswapExchangeFactory as ContractArtifact, diff --git a/contracts/integrations/test/bridges/dydx_bridge_mainnet_test.ts b/contracts/integrations/test/bridges/dydx_bridge_mainnet_test.ts index d39a483745..dc34b9c8d5 100644 --- a/contracts/integrations/test/bridges/dydx_bridge_mainnet_test.ts +++ b/contracts/integrations/test/bridges/dydx_bridge_mainnet_test.ts @@ -14,7 +14,7 @@ import { contractAddresses, dydxAccountOwner } from '../mainnet_fork_utils'; import { dydxEvents } from './abi/dydxEvents'; -blockchainTests.resets.fork('Mainnet dydx bridge tests', env => { +blockchainTests.fork.resets('Mainnet dydx bridge tests', env => { let testContract: DydxBridgeContract; // random account to receive tokens from dydx const receiver = '0x986ccf5234d9cfbb25246f1a5bfa51f4ccfcb308'; diff --git a/contracts/integrations/test/mainnet_configs_test.ts b/contracts/integrations/test/mainnet_configs_test.ts index cdc0774e63..d5fa4aacba 100644 --- a/contracts/integrations/test/mainnet_configs_test.ts +++ b/contracts/integrations/test/mainnet_configs_test.ts @@ -1,12 +1,19 @@ +import { ContractWrappers } from '@0x/contract-wrappers'; import { ERC20ProxyContract, MultiAssetProxyContract } from '@0x/contracts-asset-proxy'; import { StakingProxyContract, ZrxVaultContract } from '@0x/contracts-staking'; import { blockchainTests, describe, expect } from '@0x/contracts-test-utils'; import { AssetProxyId } from '@0x/types'; import { BigNumber } from '@0x/utils'; -import { contractAddresses, contractWrappers } from './mainnet_fork_utils'; +import { contractAddresses, getContractwrappers } from './mainnet_fork_utils'; + +blockchainTests.fork.resets('Mainnet configs tests', env => { + let contractWrappers: ContractWrappers; + + before(async () => { + contractWrappers = getContractwrappers(env.provider); + }); -blockchainTests.resets.fork('Mainnet configs tests', env => { describe('Exchange', () => { it('should be owned by the ZeroExGovernor ', async () => { const owner = await contractWrappers.exchange.owner().callAsync(); diff --git a/contracts/integrations/test/mainnet_fork_utils.ts b/contracts/integrations/test/mainnet_fork_utils.ts index c1947c25bc..77e1d5af7a 100644 --- a/contracts/integrations/test/mainnet_fork_utils.ts +++ b/contracts/integrations/test/mainnet_fork_utils.ts @@ -1,10 +1,14 @@ import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; import { ContractWrappers } from '@0x/contract-wrappers'; -import { provider } from '@0x/contracts-test-utils'; +import { Web3ProviderEngine } from '@0x/dev-utils'; const chainId = 1; -const contractAddresses = getContractAddressesForChainOrThrow(chainId); -const contractWrappers = new ContractWrappers(provider, { chainId, contractAddresses }); -const dydxAccountOwner = '0xeb58c2caa96f39626dcceb74fdbb7a9a8b54ec18'; +export const dydxAccountOwner = '0xeb58c2caa96f39626dcceb74fdbb7a9a8b54ec18'; +export const contractAddresses = getContractAddressesForChainOrThrow(chainId); -export { contractAddresses, contractWrappers, dydxAccountOwner }; +/** + * Create contract wrappers for the mainnet given a mainnet/forked provider. + */ +export function getContractwrappers(provider: Web3ProviderEngine): ContractWrappers { + return new ContractWrappers(provider, { chainId, contractAddresses }); +} diff --git a/contracts/integrations/test/wrappers.ts b/contracts/integrations/test/wrappers.ts index c8992daaa2..5caf575641 100644 --- a/contracts/integrations/test/wrappers.ts +++ b/contracts/integrations/test/wrappers.ts @@ -7,6 +7,7 @@ export * from '../test/generated-wrappers/test_dydx_user'; 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_mainnet_aggregator_fills'; 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 18c965f175..6b1f4e552c 100644 --- a/contracts/integrations/tsconfig.json +++ b/contracts/integrations/tsconfig.json @@ -8,6 +8,7 @@ "test/generated-artifacts/TestEth2Dai.json", "test/generated-artifacts/TestEth2DaiBridge.json", "test/generated-artifacts/TestFramework.json", + "test/generated-artifacts/TestMainnetAggregatorFills.json", "test/generated-artifacts/TestUniswapBridge.json", "test/generated-artifacts/TestUniswapExchange.json", "test/generated-artifacts/TestUniswapExchangeFactory.json" diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index ee84a97c0d..4abb1f9dfe 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -5,6 +5,14 @@ { "note": "Added ERC20BridgeProxy to list of unlocked accounts on mainnet fork", "pr": 2401 + }, + { + "note": "Add `blockchainTests.live()` for live network tests.", + "pr": 2407 + }, + { + "note": "Add modifiers to `blockchainTests.fork()`.", + "pr": 2407 } ] }, diff --git a/contracts/test-utils/src/mocha_blockchain.ts b/contracts/test-utils/src/mocha_blockchain.ts index 78b2f75cfa..9491907e71 100644 --- a/contracts/test-utils/src/mocha_blockchain.ts +++ b/contracts/test-utils/src/mocha_blockchain.ts @@ -1,14 +1,14 @@ -import { BlockchainLifecycle } from '@0x/dev-utils'; -import { Web3ProviderEngine } from '@0x/subproviders'; +import { BlockchainLifecycle, web3Factory } from '@0x/dev-utils'; +import { RPCSubprovider, Web3ProviderEngine } from '@0x/subproviders'; import { providerUtils } from '@0x/utils'; import { TxData, Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; import * as mocha from 'mocha'; import * as process from 'process'; -import { provider, txDefaults, web3Wrapper } from './web3_wrapper'; +import { provider, providerConfigs, txDefaults, web3Wrapper } from './web3_wrapper'; -// tslint:disable: no-namespace only-arrow-functions no-unbound-method +// tslint:disable: no-namespace only-arrow-functions no-unbound-method max-classes-per-file export type ISuite = mocha.ISuite; export type ISuiteCallbackContext = mocha.ISuiteCallbackContext; @@ -18,22 +18,32 @@ export type BlockchainSuiteCallback = (this: ISuiteCallbackContext, env: Blockch export type BlockchainContextDefinitionCallback = (description: string, callback: BlockchainSuiteCallback) => T; export interface ContextDefinition extends mocha.IContextDefinition { optional: ContextDefinitionCallback; - fork: ContextDefinitionCallback; } /** * Interface for `blockchainTests()`. */ -export interface BlockchainContextDefinition extends BlockchainContextDefinitionPartial { - resets: BlockchainContextDefinitionPartial; -} - -interface BlockchainContextDefinitionPartial { +export interface BlockchainContextDefinition { + (description: string, callback: BlockchainSuiteCallback): ISuite; only: BlockchainContextDefinitionCallback; skip: BlockchainContextDefinitionCallback; optional: BlockchainContextDefinitionCallback; - fork: BlockchainContextDefinitionCallback; - (description: string, callback: BlockchainSuiteCallback): ISuite; + resets: BlockchainContextDefinitionCallback & { + only: BlockchainContextDefinitionCallback; + skip: BlockchainContextDefinitionCallback; + optional: BlockchainContextDefinitionCallback; + }; + fork: BlockchainContextDefinitionCallback & { + only: BlockchainContextDefinitionCallback; + skip: BlockchainContextDefinitionCallback; + optional: BlockchainContextDefinitionCallback; + resets: BlockchainContextDefinitionCallback; + }; + live: BlockchainContextDefinitionCallback & { + only: BlockchainContextDefinitionCallback; + skip: BlockchainContextDefinitionCallback; + optional: BlockchainContextDefinitionCallback; + }; } /** @@ -48,29 +58,11 @@ export interface BlockchainTestsEnvironment { getAccountAddressesAsync(): Promise; } -/** - * Concret implementation of `BlockchainTestsEnvironment`. - */ -export class BlockchainTestsEnvironmentSingleton { - private static _instance: BlockchainTestsEnvironmentSingleton | undefined; - - public blockchainLifecycle: BlockchainLifecycle; - public provider: Web3ProviderEngine; - public txDefaults: Partial; - public web3Wrapper: Web3Wrapper; - - // Create or retrieve the singleton instance of this class. - public static create(): BlockchainTestsEnvironmentSingleton { - if (BlockchainTestsEnvironmentSingleton._instance === undefined) { - BlockchainTestsEnvironmentSingleton._instance = new BlockchainTestsEnvironmentSingleton(); - } - return BlockchainTestsEnvironmentSingleton._instance; - } - - // Get the singleton instance of this class. - public static getInstance(): BlockchainTestsEnvironmentSingleton | undefined { - return BlockchainTestsEnvironmentSingleton._instance; - } +class BlockchainTestsEnvironmentBase { + public blockchainLifecycle!: BlockchainLifecycle; + public provider!: Web3ProviderEngine; + public txDefaults!: Partial; + public web3Wrapper!: Web3Wrapper; public async getChainIdAsync(): Promise { return providerUtils.getChainIdAsync(this.provider); @@ -79,8 +71,33 @@ export class BlockchainTestsEnvironmentSingleton { public async getAccountAddressesAsync(): Promise { return this.web3Wrapper.getAvailableAddressesAsync(); } +} + +interface BlockchainEnvironmentFactory { + create(): BlockchainTestsEnvironment; +} + +/** + * `BlockchainTestsEnvironment` that uses the default ganache provider. + */ +export class StandardBlockchainTestsEnvironmentSingleton extends BlockchainTestsEnvironmentBase { + private static _instance: StandardBlockchainTestsEnvironmentSingleton | undefined; + + // Create or retrieve the singleton instance of this class. + public static create(): StandardBlockchainTestsEnvironmentSingleton { + if (StandardBlockchainTestsEnvironmentSingleton._instance === undefined) { + StandardBlockchainTestsEnvironmentSingleton._instance = new StandardBlockchainTestsEnvironmentSingleton(); + } + return StandardBlockchainTestsEnvironmentSingleton._instance; + } + + // Get the singleton instance of this class. + public static getInstance(): StandardBlockchainTestsEnvironmentSingleton | undefined { + return StandardBlockchainTestsEnvironmentSingleton._instance; + } protected constructor() { + super(); this.blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); this.provider = provider; this.txDefaults = txDefaults; @@ -88,6 +105,89 @@ export class BlockchainTestsEnvironmentSingleton { } } +/** + * `BlockchainTestsEnvironment` that uses a forked ganache provider. + */ +export class ForkedBlockchainTestsEnvironmentSingleton extends BlockchainTestsEnvironmentBase { + private static _instance: ForkedBlockchainTestsEnvironmentSingleton | undefined; + + // Create or retrieve the singleton instance of this class. + public static create(): ForkedBlockchainTestsEnvironmentSingleton { + if (ForkedBlockchainTestsEnvironmentSingleton._instance === undefined) { + ForkedBlockchainTestsEnvironmentSingleton._instance = new ForkedBlockchainTestsEnvironmentSingleton(); + } + return ForkedBlockchainTestsEnvironmentSingleton._instance; + } + + protected static _createWeb3Provider(forkHost: string): Web3ProviderEngine { + return web3Factory.getRpcProvider({ + ...providerConfigs, + fork: forkHost, + blockTime: 0, + }); + } + + // Get the singleton instance of this class. + public static getInstance(): ForkedBlockchainTestsEnvironmentSingleton | undefined { + return ForkedBlockchainTestsEnvironmentSingleton._instance; + } + + protected constructor() { + super(); + this.txDefaults = txDefaults; + this.provider = process.env.FORK_RPC_URL + ? ForkedBlockchainTestsEnvironmentSingleton._createWeb3Provider(process.env.FORK_RPC_URL) + : // Create a dummy provider if no RPC backend supplied. + createDummyProvider(); + this.web3Wrapper = new Web3Wrapper(this.provider); + this.blockchainLifecycle = new BlockchainLifecycle(this.web3Wrapper); + } +} + +/** + * `BlockchainTestsEnvironment` that uses a live web3 provider. + */ +export class LiveBlockchainTestsEnvironmentSingleton extends BlockchainTestsEnvironmentBase { + private static _instance: LiveBlockchainTestsEnvironmentSingleton | undefined; + + // Create or retrieve the singleton instance of this class. + public static create(): LiveBlockchainTestsEnvironmentSingleton { + if (LiveBlockchainTestsEnvironmentSingleton._instance === undefined) { + LiveBlockchainTestsEnvironmentSingleton._instance = new LiveBlockchainTestsEnvironmentSingleton(); + } + return LiveBlockchainTestsEnvironmentSingleton._instance; + } + + protected static _createWeb3Provider(rpcHost: string): Web3ProviderEngine { + const providerEngine = new Web3ProviderEngine(); + providerEngine.addProvider(new RPCSubprovider(rpcHost)); + providerUtils.startProviderEngine(providerEngine); + return providerEngine; + } + + // Get the singleton instance of this class. + public static getInstance(): LiveBlockchainTestsEnvironmentSingleton | undefined { + return LiveBlockchainTestsEnvironmentSingleton._instance; + } + + protected constructor() { + super(); + this.txDefaults = txDefaults; + this.provider = process.env.LIVE_RPC_URL + ? LiveBlockchainTestsEnvironmentSingleton._createWeb3Provider(process.env.LIVE_RPC_URL) + : // Create a dummy provider if no RPC backend supplied. + createDummyProvider(); + this.web3Wrapper = new Web3Wrapper(this.provider); + const snapshotHandlerAsync = async (): Promise => { + throw new Error('Snapshots are not supported with a live provider.'); + }; + this.blockchainLifecycle = { + startAsync: snapshotHandlerAsync, + revertAsync: snapshotHandlerAsync, + } as any; + } +} + // The original `describe()` global provided by mocha. const mochaDescribe = (global as any).describe as mocha.IContextDefinition; @@ -99,77 +199,153 @@ export const describe = _.assign(mochaDescribe, { const describeCall = process.env.TEST_ALL ? mochaDescribe : mochaDescribe.skip; return describeCall(description, callback); }, - fork(description: string, callback: SuiteCallback): ISuite | void { - const describeCall = process.env.FORK_RPC_URL ? mochaDescribe.only : mochaDescribe.skip; - return describeCall(description, callback); - }, }) as ContextDefinition; /** - * Like mocha's `describe()`, but sets up a blockchain environment on first call. + * Like mocha's `describe()`, but sets up a blockchain environment for you. */ export const blockchainTests: BlockchainContextDefinition = _.assign( function(description: string, callback: BlockchainSuiteCallback): ISuite { - return defineBlockchainSuite(description, callback, describe); + return defineBlockchainSuite(StandardBlockchainTestsEnvironmentSingleton, description, callback, describe); }, { only(description: string, callback: BlockchainSuiteCallback): ISuite { - return defineBlockchainSuite(description, callback, describe.only); - }, - skip(description: string, callback: BlockchainSuiteCallback): void { - return defineBlockchainSuite(description, callback, describe.skip); - }, - optional(description: string, callback: BlockchainSuiteCallback): ISuite | void { - return defineBlockchainSuite(description, callback, process.env.TEST_ALL ? describe : describe.skip); - }, - fork(description: string, callback: BlockchainSuiteCallback): ISuite | void { return defineBlockchainSuite( + StandardBlockchainTestsEnvironmentSingleton, description, callback, - process.env.FORK_RPC_URL ? describe.only : describe.skip, + describe.only, ); }, + skip(description: string, callback: BlockchainSuiteCallback): void { + return defineBlockchainSuite( + StandardBlockchainTestsEnvironmentSingleton, + description, + callback, + describe.skip, + ); + }, + optional(description: string, callback: BlockchainSuiteCallback): ISuite | void { + return defineBlockchainSuite( + StandardBlockchainTestsEnvironmentSingleton, + description, + callback, + process.env.TEST_ALL ? describe : describe.skip, + ); + }, + fork: _.assign( + function(description: string, callback: BlockchainSuiteCallback): ISuite | void { + return defineBlockchainSuite( + ForkedBlockchainTestsEnvironmentSingleton, + description, + callback, + process.env.FORK_RPC_URL ? describe : describe.skip, + ); + }, + { + only(description: string, callback: BlockchainSuiteCallback): ISuite | void { + return defineBlockchainSuite( + ForkedBlockchainTestsEnvironmentSingleton, + description, + callback, + process.env.FORK_RPC_URL ? describe.only : describe.skip, + ); + }, + skip(description: string, callback: BlockchainSuiteCallback): void { + return defineBlockchainSuite( + ForkedBlockchainTestsEnvironmentSingleton, + description, + callback, + describe.skip, + ); + }, + optional(description: string, callback: BlockchainSuiteCallback): ISuite | void { + return defineBlockchainSuite( + ForkedBlockchainTestsEnvironmentSingleton, + description, + callback, + process.env.FORK_RPC_URL ? describe.optional : describe.skip, + ); + }, + resets(description: string, callback: BlockchainSuiteCallback): ISuite | void { + return defineResetsBlockchainSuite( + ForkedBlockchainTestsEnvironmentSingleton, + description, + callback, + process.env.FORK_RPC_URL ? describe : describe.skip, + ); + }, + }, + ), + live: _.assign( + function(description: string, callback: BlockchainSuiteCallback): ISuite | void { + return defineBlockchainSuite( + LiveBlockchainTestsEnvironmentSingleton, + description, + callback, + process.env.LIVE_RPC_URL ? describe : describe.skip, + ); + }, + { + only(description: string, callback: BlockchainSuiteCallback): ISuite | void { + return defineBlockchainSuite( + LiveBlockchainTestsEnvironmentSingleton, + description, + callback, + process.env.LIVE_RPC_URL ? describe.only : describe.skip, + ); + }, + skip(description: string, callback: BlockchainSuiteCallback): void { + return defineBlockchainSuite( + LiveBlockchainTestsEnvironmentSingleton, + description, + callback, + describe.skip, + ); + }, + optional(description: string, callback: BlockchainSuiteCallback): ISuite | void { + return defineBlockchainSuite( + LiveBlockchainTestsEnvironmentSingleton, + description, + callback, + process.env.LIVE_RPC_URL ? describe.optional : describe.skip, + ); + }, + }, + ), resets: _.assign( function(description: string, callback: BlockchainSuiteCallback): ISuite { - return defineBlockchainSuite(description, callback, function( - _description: string, - _callback: SuiteCallback, - ): ISuite { - return defineResetsSuite(_description, _callback, describe); - }); + return defineResetsBlockchainSuite( + StandardBlockchainTestsEnvironmentSingleton, + description, + callback, + describe, + ); }, { only(description: string, callback: BlockchainSuiteCallback): ISuite { - return defineBlockchainSuite(description, callback, function( - _description: string, - _callback: SuiteCallback, - ): ISuite { - return defineResetsSuite(_description, _callback, describe.only); - }); + return defineResetsBlockchainSuite( + StandardBlockchainTestsEnvironmentSingleton, + description, + callback, + describe.only, + ); }, skip(description: string, callback: BlockchainSuiteCallback): void { - return defineBlockchainSuite(description, callback, function( - _description: string, - _callback: SuiteCallback, - ): void { - return defineResetsSuite(_description, _callback, describe.skip); - }); + return defineResetsBlockchainSuite( + StandardBlockchainTestsEnvironmentSingleton, + description, + callback, + describe.skip, + ); }, optional(description: string, callback: BlockchainSuiteCallback): ISuite | void { - return defineBlockchainSuite(description, callback, function( - _description: string, - _callback: SuiteCallback, - ): ISuite | void { - return defineResetsSuite(_description, _callback, describe.optional); - }); - }, - fork(description: string, callback: BlockchainSuiteCallback): ISuite | void { - return defineBlockchainSuite(description, callback, function( - _description: string, - _callback: SuiteCallback, - ): ISuite | void { - return defineResetsSuite(_description, _callback, describe.fork); - }); + return defineResetsBlockchainSuite( + StandardBlockchainTestsEnvironmentSingleton, + description, + callback, + describe.optional, + ); }, }, ), @@ -177,29 +353,37 @@ export const blockchainTests: BlockchainContextDefinition = _.assign( ) as BlockchainContextDefinition; function defineBlockchainSuite( + envFactory: BlockchainEnvironmentFactory, description: string, callback: BlockchainSuiteCallback, describeCall: ContextDefinitionCallback, ): T { - const env = BlockchainTestsEnvironmentSingleton.create(); return describeCall(description, function(this: ISuiteCallbackContext): void { - before(async () => env.blockchainLifecycle.startAsync()); - after(async () => env.blockchainLifecycle.revertAsync()); + callback.call(this, envFactory.create()); + }); +} + +function defineResetsBlockchainSuite( + envFactory: BlockchainEnvironmentFactory, + description: string, + callback: BlockchainSuiteCallback, + describeCall: ContextDefinitionCallback, +): T { + return describeCall(description, function(this: ISuiteCallbackContext): void { + const env = envFactory.create(); + beforeEach(async () => env.blockchainLifecycle.startAsync()); + afterEach(async () => env.blockchainLifecycle.revertAsync()); callback.call(this, env); }); } -function defineResetsSuite( - description: string, - callback: SuiteCallback, - describeCall: ContextDefinitionCallback, -): T { - return describeCall(description, function(this: ISuiteCallbackContext): void { - const env = BlockchainTestsEnvironmentSingleton.getInstance(); - if (env !== undefined) { - beforeEach(async () => env.blockchainLifecycle.startAsync()); - afterEach(async () => env.blockchainLifecycle.revertAsync()); - } - callback.call(this); - }); +function createDummyProvider(): Web3ProviderEngine { + return { + addProvider: _.noop, + on: _.noop, + send: _.noop, + sendAsync: _.noop, + start: _.noop, + stop: _.noop, + }; } diff --git a/contracts/test-utils/src/web3_wrapper.ts b/contracts/test-utils/src/web3_wrapper.ts index a918013b46..9ac3dbbb42 100644 --- a/contracts/test-utils/src/web3_wrapper.ts +++ b/contracts/test-utils/src/web3_wrapper.ts @@ -15,28 +15,12 @@ export const txDefaults = { gasPrice: constants.DEFAULT_GAS_PRICE, }; -let providerConfigs: Web3Config = { +export let providerConfigs: Web3Config = { total_accounts: constants.NUM_TEST_ACCOUNTS, shouldUseInProcessGanache: true, shouldAllowUnlimitedContractSize: true, }; -if (process.env.FORK_RPC_URL !== undefined) { - providerConfigs = { - ...providerConfigs, - fork: process.env.FORK_RPC_URL, - blockTime: 0, - unlocked_accounts: [ - // ZeroExGovernor signer addresses - '0x257619b7155d247e43c8b6d90c8c17278ae481f0', - '0x5ee2a00f8f01d099451844af7f894f26a57fcbf2', - '0x894d623e0e0e8ed12c4a73dada999e275684a37d', - // ERC20BridgeProxy - '0x8ed95d1746bf1e4dab58d8ed4724f1ef95b20db0', - ], - }; -} - export const provider: Web3ProviderEngine = web3Factory.getRpcProvider(providerConfigs); provider.stop(); const isCoverageEnabled = env.parseBoolean(EnvVars.SolidityCoverage); diff --git a/packages/dev-utils/CHANGELOG.json b/packages/dev-utils/CHANGELOG.json index cf50616685..f071056cac 100644 --- a/packages/dev-utils/CHANGELOG.json +++ b/packages/dev-utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "3.1.0", + "changes": [ + { + "note": "Add `locked` `Web3Config` option.", + "pr": 2407 + } + ] + }, { "timestamp": 1576540892, "version": "3.0.2", diff --git a/packages/dev-utils/src/web3_factory.ts b/packages/dev-utils/src/web3_factory.ts index f0edade21a..e2750389ec 100644 --- a/packages/dev-utils/src/web3_factory.ts +++ b/packages/dev-utils/src/web3_factory.ts @@ -23,6 +23,7 @@ export interface Web3Config { shouldAllowUnlimitedContractSize?: boolean; fork?: string; blockTime?: number; + locked?: boolean; unlocked_accounts?: string[]; } @@ -78,6 +79,7 @@ export const web3Factory = { mnemonic: 'concert load couple harbor equip island argue ramp clarify fence smart topic', fork: config.fork, blockTime: config.blockTime, + locked: config.locked, unlocked_accounts: config.unlocked_accounts, } as any), // TODO remove any once types are merged in DefinitelyTyped );