Add aggregator mainnet tests (#2407)

* `@0x/contracts-erc20-bridge-sampler`: Add gas limits to external quote calls.
`@0x/contract-addresses`: Point `erc20BridgeSampler` to new version.

* `@0x/contracts-utils`: Add kovan addresses to `DeploymentConstants`.
`@0x/contract-addresses`: Add kovan `ERC20BridgeSampler` address.

* `@0x/contracts-erc20-bridge-sampler`: Fix changelog.

* `@0x/asset-swapper`: Ignore zero sample results from the sampler contract.
`@0x/asset-swapper`: Allow skipping Uniswap when dealing with low precision amounts with `minUniswapDecimals` option.
`@0x/asset-swapper`: Increase default `runLimit` from `1024` to `4096`.
`@0x/asset-swapper`: Increase default `numSamples` from `8` to `10`
`@0x/asset-swapper`: Fix ordering of optimized orders.
`@0x/asset-swapper`: Fix best and worst quotes being reversed sometimes.
`@0x/asset-swapper`: Fix rounding of quoted asset amounts.

* `@0x/asset-swapper`: Change default `minUniswapDecimals` option from 8 to 7.

* `@0x/asset-swapper`: Revert uniswap decimals fix.

* `@0x/contracts-test-utils`: Add `blockchainTests.live()` for live network tests.
`@0x/contracts-test-utils`: Add modifiers to `blockchainTests.fork()`.
`@0x/contracts-integrations`: Add aggregator mainnet tests.

* `@0x/contracts-integrations`: Fix `fork/resets` modifier ordering on dydx tests.
`@0x/contracts-integrations`: Move and tweak aggregation tests.

* `@0x/contracts-integrations`: Handle non-responsive third-party SRA ordebooks with a little more grace.

* `@0x/contracts-integrations`: Fix linter error.

* `@0x/contracts-test-utils`: Consolidate fork provider logic into `mocha_blockchain.ts`.

* `@0x/contracts-integrations`: Run prettier on aggregation fill tests.

* `@0x/dev-utils`: Add `locked` to `Web3Config`.

* `@0x/contracts-integrations`: Update mainnet fork tests.
`@0x/contracts-test-utils`: Fix forked tests being skipped.
`@0x/contracts-erc20-bridge-sampler`: Regenerate artifacts.

* `@0x/contracts-test-utils`: Remove unecessary `locked` option when creating forked ganache provider.

* Fix redundant zero check

* Set fee amount in fillable amounts test

Co-authored-by: Jacob Evans <dekz@dekz.net>
This commit is contained in:
Lawrence Forman 2020-01-03 23:47:40 -05:00 committed by GitHub
parent 0571a96cea
commit ff2cc8c887
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 828 additions and 125 deletions

View File

@ -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
}
]
},

View File

@ -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);
}
}
}

View File

@ -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",

View File

@ -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<SignedOrder[]> {
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<SignedOrder[]> {
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<SignedOrder[]> {
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,
);
}
});
}
}
});
});

View File

@ -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,
},
};

View File

@ -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,

View File

@ -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';

View File

@ -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();

View File

@ -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 });
}

View File

@ -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';

View File

@ -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"

View File

@ -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
}
]
},

View File

@ -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<T> = (description: string, callback: BlockchainSuiteCallback) => T;
export interface ContextDefinition extends mocha.IContextDefinition {
optional: ContextDefinitionCallback<ISuite | void>;
fork: ContextDefinitionCallback<ISuite | void>;
}
/**
* Interface for `blockchainTests()`.
*/
export interface BlockchainContextDefinition extends BlockchainContextDefinitionPartial {
resets: BlockchainContextDefinitionPartial;
}
interface BlockchainContextDefinitionPartial {
export interface BlockchainContextDefinition {
(description: string, callback: BlockchainSuiteCallback): ISuite;
only: BlockchainContextDefinitionCallback<ISuite>;
skip: BlockchainContextDefinitionCallback<void>;
optional: BlockchainContextDefinitionCallback<ISuite | void>;
fork: BlockchainContextDefinitionCallback<ISuite | void>;
(description: string, callback: BlockchainSuiteCallback): ISuite;
resets: BlockchainContextDefinitionCallback<ISuite | void> & {
only: BlockchainContextDefinitionCallback<ISuite>;
skip: BlockchainContextDefinitionCallback<void>;
optional: BlockchainContextDefinitionCallback<ISuite | void>;
};
fork: BlockchainContextDefinitionCallback<ISuite | void> & {
only: BlockchainContextDefinitionCallback<ISuite>;
skip: BlockchainContextDefinitionCallback<void>;
optional: BlockchainContextDefinitionCallback<ISuite | void>;
resets: BlockchainContextDefinitionCallback<ISuite | void>;
};
live: BlockchainContextDefinitionCallback<ISuite | void> & {
only: BlockchainContextDefinitionCallback<ISuite>;
skip: BlockchainContextDefinitionCallback<void>;
optional: BlockchainContextDefinitionCallback<ISuite | void>;
};
}
/**
@ -48,29 +58,11 @@ export interface BlockchainTestsEnvironment {
getAccountAddressesAsync(): Promise<string[]>;
}
/**
* Concret implementation of `BlockchainTestsEnvironment`.
*/
export class BlockchainTestsEnvironmentSingleton {
private static _instance: BlockchainTestsEnvironmentSingleton | undefined;
public blockchainLifecycle: BlockchainLifecycle;
public provider: Web3ProviderEngine;
public txDefaults: Partial<TxData>;
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<TxData>;
public web3Wrapper!: Web3Wrapper;
public async getChainIdAsync(): Promise<number> {
return providerUtils.getChainIdAsync(this.provider);
@ -79,8 +71,33 @@ export class BlockchainTestsEnvironmentSingleton {
public async getAccountAddressesAsync(): Promise<string[]> {
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<void> => {
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<T>(
envFactory: BlockchainEnvironmentFactory,
description: string,
callback: BlockchainSuiteCallback,
describeCall: ContextDefinitionCallback<T>,
): 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<T>(
envFactory: BlockchainEnvironmentFactory,
description: string,
callback: BlockchainSuiteCallback,
describeCall: ContextDefinitionCallback<T>,
): 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<T>(
description: string,
callback: SuiteCallback,
describeCall: ContextDefinitionCallback<T>,
): 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,
};
}

View File

@ -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);

View File

@ -1,4 +1,13 @@
[
{
"version": "3.1.0",
"changes": [
{
"note": "Add `locked` `Web3Config` option.",
"pr": 2407
}
]
},
{
"timestamp": 1576540892,
"version": "3.0.2",

View File

@ -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
);