integration tests

This commit is contained in:
Michael Zhu
2019-11-18 14:44:31 -08:00
parent d51bbb0008
commit bb5885e2bb
23 changed files with 872 additions and 58 deletions

View File

@@ -0,0 +1,51 @@
/*
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
{
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);
buyToken.transfer(
msg.sender,
minimumFillAmount
);
return minimumFillAmount;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,102 @@
/*
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;
constructor(address _tokenAddress) public {
token = DummyERC20Token(_tokenAddress);
}
// solhint-disable no-empty-blocks
/// @dev Used to receive ETH for testing.
function topUpEth()
external
payable
{}
function ethToTokenTransferInput(
uint256 minTokensBought,
uint256, /* deadline */
address recipient
)
external
payable
returns (uint256 tokensBought)
{
token.mint(minTokensBought);
token.transfer(recipient, minTokensBought);
return minTokensBought;
}
function tokenToEthSwapInput(
uint256 tokensSold,
uint256 minEthBought,
uint256 /* deadline */
)
external
returns (uint256 ethBought)
{
token.transferFrom(
msg.sender,
address(this),
tokensSold
);
msg.sender.transfer(minEthBought);
return minEthBought;
}
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);
toToken.transfer(recipient, minTokensBought);
return minTokensBought;
}
function toTokenAddress()
external
view
returns (address _tokenAddress)
{
return address(token);
}
}

View File

@@ -0,0 +1,53 @@
/*
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-asset-proxy/contracts/src/interfaces/IUniswapExchange.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 (IUniswapExchange)
{
return IUniswapExchange(_testExchanges[tokenAddress]);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
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';
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];
}

View File

@@ -0,0 +1,47 @@
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';
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];
}

View File

@@ -109,7 +109,7 @@ blockchainTests.resets('Coordinator integration tests', env => {
msgValue?: BigNumber,
): Promise<LocalBalanceStore> {
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));

View File

@@ -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 () => {

View File

@@ -72,7 +72,7 @@ export class FillOrderWrapper {
initBalanceStore: BalanceStore,
opts: { takerAssetFillAmount?: BigNumber } = {},
): Promise<[FillResults, FillEventArgs, BalanceStore]> {
const balanceStore = LocalBalanceStore.create(this._devUtils, initBalanceStore);
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.

View File

@@ -118,7 +118,7 @@ blockchainTests.resets('fillOrder integration tests', env => {
msgValue?: BigNumber,
): Promise<LocalBalanceStore> {
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();

View File

@@ -0,0 +1,352 @@
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 { deployForwarderAsync } from './deploy_forwarder';
import { ForwarderTestFactory } from './forwarder_test_factory';
blockchainTests.resets('Forwarder <> ERC20Bridge integration tests', env => {
let deployment: DeploymentManager;
let forwarder: ForwarderContract;
let assetDataEncoder: IAssetDataContract;
let balanceStore: BlockchainBalanceStore;
let testFactory: ForwarderTestFactory;
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 [eth2DaiBridge] = await deployEth2DaiBridgeAsync(deployment, env);
const [uniswapBridge, [uniswapMakerTokenExchange]] = await deployUniswapBridgeAsync(deployment, env, [
makerToken.address,
]);
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 uniswapMakerTokenExchange.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 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 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 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 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

View File

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

View File

@@ -110,11 +110,8 @@ blockchainTests('Forwarder integration tests', env => {
forwarder,
deployment,
balanceStore,
maker,
taker,
orderFeeRecipient,
forwarderFeeRecipient,
devUtils,
);
});
@@ -138,7 +135,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 +199,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 +518,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 +575,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,

View File

@@ -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,7 +27,7 @@ interface ForwarderFillState {
}
interface MarketSellOptions {
forwarderFeePercentage: BigNumber;
forwarderFeePercentage: Numberish;
revertError: RevertError;
}
@@ -28,16 +35,32 @@ interface MarketBuyOptions extends MarketSellOptions {
ethValueAdjustment: number; // Used to provided insufficient/excess ETH
}
function isPercentageFee(takerFeeAssetData: string, makerAssetData: string): boolean {
const makerAssetProxyId = hexSlice(makerAssetData, 0, 4);
const takerFeeAssetProxyId = hexSlice(takerFeeAssetData, 0, 4);
if (makerAssetProxyId === AssetProxyId.ERC20Bridge && takerFeeAssetProxyId === AssetProxyId.ERC20) {
const assetDataDecoder = new IAssetDataContract(constants.NULL_ADDRESS, provider);
const [makerTokenAddress] = assetDataDecoder.getABIDecodedTransactionData<string>(
'ERC20Bridge',
makerAssetData,
);
const takerFeeTokenAddress = assetDataDecoder.getABIDecodedTransactionData<string>(
'ERC20Token',
takerFeeAssetData,
);
return makerTokenAddress === takerFeeTokenAddress;
} else {
return makerAssetData === takerFeeAssetData;
}
}
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(
@@ -164,7 +187,7 @@ export class ForwarderTestFactory {
options: Partial<MarketBuyOptions>,
): Promise<ForwarderFillState> {
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,
@@ -230,7 +253,7 @@ export class ForwarderTestFactory {
let wethSpentAmount = takerAssetAmount.plus(DeploymentManager.protocolFee);
let makerAssetAcquiredAmount = makerAssetAmount;
if (order.takerFeeAssetData === order.makerAssetData) {
if (isPercentageFee(order.takerFeeAssetData, order.makerAssetData)) {
makerAssetAcquiredAmount = makerAssetAcquiredAmount.minus(takerFee);
} else if (order.takerFeeAssetData === order.takerAssetData) {
wethSpentAmount = wethSpentAmount.plus(takerFee);
@@ -244,29 +267,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,
);

View File

@@ -36,7 +36,7 @@ export function validStakeAssertion(
return new FunctionAssertion(stakingWrapper.stake, {
before: async (amount: BigNumber, txData: Partial<TxData>) => {
// 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,

View File

@@ -36,7 +36,7 @@ export function validUnstakeAssertion(
return new FunctionAssertion(stakingWrapper.unstake, {
before: async (amount: BigNumber, txData: Partial<TxData>) => {
// 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,

View File

@@ -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 _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<TokenContractsByName> = {},
) {
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<void> {
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<string>(
'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]);

View File

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

View File

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

View File

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