Merge pull request #2356 from 0xProject/feature/forwarder/erc20-bridge-buy

`@0x/contracts-exchange-forwarder`: ERC20Bridge buy support in Forwarder
This commit is contained in:
mzhu25
2019-11-26 15:20:54 -08:00
committed by GitHub
40 changed files with 1378 additions and 177 deletions

View File

@@ -2,6 +2,8 @@ export {
DummyERC721ReceiverContract,
DummyERC721TokenContract,
ERC721TokenContract,
ERC721TokenEvents,
ERC721TokenTransferEventArgs,
IERC721ReceiverContract,
} from './wrappers';
export { artifacts } from './artifacts';

View File

@@ -1,4 +1,13 @@
[
{
"version": "3.1.0-beta.4",
"changes": [
{
"note": "Added buy support for ERC20Bridge",
"pr": 2356
}
]
},
{
"version": "3.1.0-beta.3",
"changes": [

View File

@@ -32,13 +32,13 @@ contract Forwarder is
{
constructor (
address _exchange,
bytes memory _wethAssetData
address _weth
)
public
Ownable()
LibConstants(
_exchange,
_wethAssetData
_weth
)
MixinForwarderCore()
{}

View File

@@ -21,9 +21,9 @@ pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/Ownable.sol";
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol";
import "@0x/contracts-erc721/contracts/src/interfaces/IERC721Token.sol";
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol";
import "./libs/LibConstants.sol";
import "./libs/LibForwarderRichErrors.sol";
import "./interfaces/IAssets.sol";
@@ -36,13 +36,10 @@ contract MixinAssets is
{
using LibBytes for bytes;
bytes4 constant internal ERC20_TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)"));
/// @dev Withdraws assets from this contract. The contract formerly required a ZRX balance in order
/// to function optimally, and this function allows the ZRX to be withdrawn by owner.
/// It may also be used to withdraw assets that were accidentally sent to this contract.
/// @dev Withdraws assets from this contract. It may be used by the owner to withdraw assets
/// that were accidentally sent to this contract.
/// @param assetData Byte array encoded for the respective asset proxy.
/// @param amount Amount of ERC20 token to withdraw.
/// @param amount Amount of the asset to withdraw.
function withdrawAsset(
bytes calldata assetData,
uint256 amount
@@ -63,14 +60,16 @@ contract MixinAssets is
external
{
bytes4 proxyId = assetData.readBytes4(0);
bytes4 erc20ProxyId = IAssetData(address(0)).ERC20Token.selector;
// For now we only care about ERC20, since percentage fees on ERC721 tokens are invalid.
if (proxyId == ERC20_DATA_ID) {
address proxyAddress = EXCHANGE.getAssetProxy(ERC20_DATA_ID);
if (proxyId == erc20ProxyId) {
address proxyAddress = EXCHANGE.getAssetProxy(erc20ProxyId);
if (proxyAddress == address(0)) {
LibRichErrors.rrevert(LibForwarderRichErrors.UnregisteredAssetProxyError());
}
IERC20Token assetToken = IERC20Token(assetData.readAddress(16));
assetToken.approve(proxyAddress, MAX_UINT);
address token = assetData.readAddress(16);
LibERC20Token.approve(token, proxyAddress, MAX_UINT);
}
}
@@ -85,9 +84,12 @@ contract MixinAssets is
{
bytes4 proxyId = assetData.readBytes4(0);
if (proxyId == ERC20_DATA_ID) {
if (
proxyId == IAssetData(address(0)).ERC20Token.selector ||
proxyId == IAssetData(address(0)).ERC20Bridge.selector
) {
_transferERC20Token(assetData, amount);
} else if (proxyId == ERC721_DATA_ID) {
} else if (proxyId == IAssetData(address(0)).ERC721Token.selector) {
_transferERC721Token(assetData, amount);
} else {
LibRichErrors.rrevert(LibForwarderRichErrors.UnsupportedAssetProxyError(
@@ -96,7 +98,7 @@ contract MixinAssets is
}
}
/// @dev Decodes ERC20 assetData and transfers given amount to sender.
/// @dev Decodes ERC20 or ERC20Bridge assetData and transfers given amount to sender.
/// @param assetData Byte array encoded for the respective asset proxy.
/// @param amount Amount of asset to transfer to sender.
function _transferERC20Token(

View File

@@ -19,12 +19,15 @@
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibFillResults.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol";
import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol";
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol";
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "./libs/LibConstants.sol";
import "./libs/LibForwarderRichErrors.sol";
import "./MixinAssets.sol";
@@ -34,6 +37,7 @@ contract MixinExchangeWrapper is
LibConstants,
MixinAssets
{
using LibBytes for bytes;
using LibSafeMath for uint256;
/// @dev Fills the input order.
@@ -88,7 +92,10 @@ contract MixinExchangeWrapper is
)
{
// No taker fee or percentage fee
if (order.takerFee == 0 || order.takerFeeAssetData.equals(order.makerAssetData)) {
if (
order.takerFee == 0 ||
_areUnderlyingAssetsEqual(order.takerFeeAssetData, order.makerAssetData)
) {
// Attempt to sell the remaining amount of WETH
LibFillResults.FillResults memory singleFillResults = _fillOrderNoThrow(
order,
@@ -103,7 +110,7 @@ contract MixinExchangeWrapper is
makerAssetAcquiredAmount = singleFillResults.makerAssetFilledAmount
.safeSub(singleFillResults.takerFeePaid);
// WETH fee
} else if (order.takerFeeAssetData.equals(order.takerAssetData)) {
} else if (_areUnderlyingAssetsEqual(order.takerFeeAssetData, order.takerAssetData)) {
// We will first sell WETH as the takerAsset, then use it to pay the takerFee.
// This ensures that we reserve enough to pay the taker and protocol fees.
@@ -150,9 +157,10 @@ contract MixinExchangeWrapper is
uint256 totalMakerAssetAcquiredAmount
)
{
uint256 ordersLength = orders.length;
uint256 protocolFee = tx.gasprice.safeMul(EXCHANGE.protocolFeeMultiplier());
bytes4 erc20BridgeProxyId = IAssetData(address(0)).ERC20Bridge.selector;
uint256 ordersLength = orders.length;
for (uint256 i = 0; i != ordersLength; i++) {
// Preemptively skip to avoid division by zero in _marketSellSingleOrder
if (orders[i].makerAssetAmount == 0 || orders[i].takerAssetAmount == 0) {
@@ -164,6 +172,15 @@ contract MixinExchangeWrapper is
.safeSub(totalWethSpentAmount)
.safeSub(protocolFee);
// If the maker asset is ERC20Bridge, take a snapshot of the Forwarder contract's balance.
bytes4 makerAssetProxyId = orders[i].makerAssetData.readBytes4(0);
address tokenAddress;
uint256 balanceBefore;
if (makerAssetProxyId == erc20BridgeProxyId) {
tokenAddress = orders[i].makerAssetData.readAddress(16);
balanceBefore = IERC20Token(tokenAddress).balanceOf(address(this));
}
(
uint256 wethSpentAmount,
uint256 makerAssetAcquiredAmount
@@ -173,6 +190,15 @@ contract MixinExchangeWrapper is
remainingTakerAssetFillAmount
);
// Account for the ERC20Bridge transfering more of the maker asset than expected.
if (makerAssetProxyId == erc20BridgeProxyId) {
uint256 balanceAfter = IERC20Token(tokenAddress).balanceOf(address(this));
makerAssetAcquiredAmount = LibSafeMath.max256(
balanceAfter.safeSub(balanceBefore),
makerAssetAcquiredAmount
);
}
_transferAssetToSender(orders[i].makerAssetData, makerAssetAcquiredAmount);
totalWethSpentAmount = totalWethSpentAmount
@@ -206,7 +232,10 @@ contract MixinExchangeWrapper is
)
{
// No taker fee or WETH fee
if (order.takerFee == 0 || order.takerFeeAssetData.equals(order.takerAssetData)) {
if (
order.takerFee == 0 ||
_areUnderlyingAssetsEqual(order.takerFeeAssetData, order.takerAssetData)
) {
// Calculate the remaining amount of takerAsset to sell
uint256 remainingTakerAssetFillAmount = LibMath.getPartialAmountCeil(
order.takerAssetAmount,
@@ -228,7 +257,7 @@ contract MixinExchangeWrapper is
makerAssetAcquiredAmount = singleFillResults.makerAssetFilledAmount;
// Percentage fee
} else if (order.takerFeeAssetData.equals(order.makerAssetData)) {
} else if (_areUnderlyingAssetsEqual(order.takerFeeAssetData, order.makerAssetData)) {
// Calculate the remaining amount of takerAsset to sell
uint256 remainingTakerAssetFillAmount = LibMath.getPartialAmountCeil(
order.takerAssetAmount,
@@ -277,6 +306,8 @@ contract MixinExchangeWrapper is
uint256 totalMakerAssetAcquiredAmount
)
{
bytes4 erc20BridgeProxyId = IAssetData(address(0)).ERC20Bridge.selector;
uint256 ordersLength = orders.length;
for (uint256 i = 0; i != ordersLength; i++) {
// Preemptively skip to avoid division by zero in _marketBuySingleOrder
@@ -287,6 +318,15 @@ contract MixinExchangeWrapper is
uint256 remainingMakerAssetFillAmount = makerAssetBuyAmount
.safeSub(totalMakerAssetAcquiredAmount);
// If the maker asset is ERC20Bridge, take a snapshot of the Forwarder contract's balance.
bytes4 makerAssetProxyId = orders[i].makerAssetData.readBytes4(0);
address tokenAddress;
uint256 balanceBefore;
if (makerAssetProxyId == erc20BridgeProxyId) {
tokenAddress = orders[i].makerAssetData.readAddress(16);
balanceBefore = IERC20Token(tokenAddress).balanceOf(address(this));
}
(
uint256 wethSpentAmount,
uint256 makerAssetAcquiredAmount
@@ -296,6 +336,15 @@ contract MixinExchangeWrapper is
remainingMakerAssetFillAmount
);
// Account for the ERC20Bridge transfering more of the maker asset than expected.
if (makerAssetProxyId == erc20BridgeProxyId) {
uint256 balanceAfter = IERC20Token(tokenAddress).balanceOf(address(this));
makerAssetAcquiredAmount = LibSafeMath.max256(
balanceAfter.safeSub(balanceBefore),
makerAssetAcquiredAmount
);
}
_transferAssetToSender(orders[i].makerAssetData, makerAssetAcquiredAmount);
totalWethSpentAmount = totalWethSpentAmount
@@ -316,4 +365,36 @@ contract MixinExchangeWrapper is
));
}
}
/// @dev Checks whether one asset is effectively equal to another asset.
/// This is the case if they have the same ERC20Proxy/ERC20BridgeProxy asset data, or if
/// one is the ERC20Bridge equivalent of the other.
/// @param assetData1 Byte array encoded for the takerFee asset proxy.
/// @param assetData2 Byte array encoded for the maker asset proxy.
/// @return areEqual Whether or not the underlying assets are equal.
function _areUnderlyingAssetsEqual(
bytes memory assetData1,
bytes memory assetData2
)
internal
pure
returns (bool)
{
bytes4 assetProxyId1 = assetData1.readBytes4(0);
bytes4 assetProxyId2 = assetData2.readBytes4(0);
bytes4 erc20ProxyId = IAssetData(address(0)).ERC20Token.selector;
bytes4 erc20BridgeProxyId = IAssetData(address(0)).ERC20Bridge.selector;
if (
(assetProxyId1 == erc20ProxyId || assetProxyId1 == erc20BridgeProxyId) &&
(assetProxyId2 == erc20ProxyId || assetProxyId2 == erc20BridgeProxyId)
) {
// Compare the underlying token addresses.
address token1 = assetData1.readAddress(16);
address token2 = assetData2.readAddress(16);
return (token1 == token2);
} else {
return false;
}
}
}

View File

@@ -24,6 +24,7 @@ import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol";
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol";
import "./libs/LibConstants.sol";
import "./libs/LibForwarderRichErrors.sol";
import "./interfaces/IAssets.sol";
@@ -46,7 +47,7 @@ contract MixinForwarderCore is
constructor ()
public
{
address proxyAddress = EXCHANGE.getAssetProxy(ERC20_DATA_ID);
address proxyAddress = EXCHANGE.getAssetProxy(IAssetData(address(0)).ERC20Token.selector);
if (proxyAddress == address(0)) {
LibRichErrors.rrevert(LibForwarderRichErrors.UnregisteredAssetProxyError());
}

View File

@@ -27,8 +27,6 @@ contract LibConstants {
using LibBytes for bytes;
bytes4 constant internal ERC20_DATA_ID = bytes4(keccak256("ERC20Token(address)"));
bytes4 constant internal ERC721_DATA_ID = bytes4(keccak256("ERC721Token(address,uint256)"));
uint256 constant internal MAX_UINT = 2**256 - 1;
uint256 constant internal PERCENTAGE_DENOMINATOR = 10**18;
uint256 constant internal MAX_FEE_PERCENTAGE = 5 * PERCENTAGE_DENOMINATOR / 100; // 5%
@@ -36,19 +34,15 @@ contract LibConstants {
// solhint-disable var-name-mixedcase
IExchange internal EXCHANGE;
IEtherToken internal ETHER_TOKEN;
bytes internal WETH_ASSET_DATA;
// solhint-enable var-name-mixedcase
constructor (
address _exchange,
bytes memory _wethAssetData
address _weth
)
public
{
EXCHANGE = IExchange(_exchange);
WETH_ASSET_DATA = _wethAssetData;
address etherToken = _wethAssetData.readAddress(16);
ETHER_TOKEN = IEtherToken(etherToken);
ETHER_TOKEN = IEtherToken(_weth);
}
}

View File

@@ -18,8 +18,6 @@
pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
library LibForwarderRichErrors {

View File

@@ -0,0 +1,63 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "../src/MixinExchangeWrapper.sol";
import "../src/libs/LibConstants.sol";
contract TestForwarder is
LibConstants,
MixinExchangeWrapper
{
// solhint-disable no-empty-blocks
constructor ()
public
LibConstants(
address(0),
address(0)
)
{}
function areUnderlyingAssetsEqual(
bytes memory assetData1,
bytes memory assetData2
)
public
returns (bool)
{
return _areUnderlyingAssetsEqual(
assetData1,
assetData2
);
}
function transferAssetToSender(
bytes memory assetData,
uint256 amount
)
public
{
_transferAssetToSender(
assetData,
amount
);
}
}

View File

@@ -14,11 +14,12 @@
"build:ts": "tsc -b",
"build:ci": "yarn build",
"pre_build": "run-s compile contracts:gen generate_contract_wrappers contracts:copy",
"test": "echo !!! Tests have been relocated to @0x/contracts-integrations !!!",
"test": "yarn run_mocha",
"rebuild_and_test": "run-s build test",
"test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov",
"test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html",
"test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha",
"run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit",
"compile": "sol-compiler",
"watch": "sol-compiler -w",
"clean": "shx rm -rf lib test/generated-artifacts test/generated-wrappers generated-artifacts generated-wrappers",
@@ -38,7 +39,7 @@
},
"config": {
"publicInterfaceContracts": "Forwarder",
"abis": "./test/generated-artifacts/@(Forwarder|IAssets|IForwarder|IForwarderCore|LibConstants|LibForwarderRichErrors|MixinAssets|MixinExchangeWrapper|MixinForwarderCore|MixinWeth).json",
"abis": "./test/generated-artifacts/@(Forwarder|IAssets|IForwarder|IForwarderCore|LibConstants|LibForwarderRichErrors|MixinAssets|MixinExchangeWrapper|MixinForwarderCore|MixinWeth|TestForwarder).json",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
},
"repository": {

View File

@@ -15,6 +15,7 @@ import * as MixinAssets from '../test/generated-artifacts/MixinAssets.json';
import * as MixinExchangeWrapper from '../test/generated-artifacts/MixinExchangeWrapper.json';
import * as MixinForwarderCore from '../test/generated-artifacts/MixinForwarderCore.json';
import * as MixinWeth from '../test/generated-artifacts/MixinWeth.json';
import * as TestForwarder from '../test/generated-artifacts/TestForwarder.json';
export const artifacts = {
Forwarder: Forwarder as ContractArtifact,
MixinAssets: MixinAssets as ContractArtifact,
@@ -26,4 +27,5 @@ export const artifacts = {
IForwarderCore: IForwarderCore as ContractArtifact,
LibConstants: LibConstants as ContractArtifact,
LibForwarderRichErrors: LibForwarderRichErrors as ContractArtifact,
TestForwarder: TestForwarder as ContractArtifact,
};

View File

@@ -0,0 +1,184 @@
import { IAssetDataContract } from '@0x/contracts-asset-proxy';
import {
artifacts as ERC20Artifacts,
DummyERC20TokenContract,
ERC20TokenEvents,
ERC20TokenTransferEventArgs,
} from '@0x/contracts-erc20';
import {
artifacts as ERC721Artifacts,
DummyERC721TokenContract,
ERC721TokenEvents,
ERC721TokenTransferEventArgs,
} from '@0x/contracts-erc721';
import {
blockchainTests,
constants,
expect,
getRandomInteger,
hexRandom,
hexSlice,
randomAddress,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { ForwarderRevertErrors } from '../src';
import { artifacts } from './artifacts';
import { TestForwarderContract } from './wrappers';
blockchainTests('Supported asset type unit tests', env => {
let forwarder: TestForwarderContract;
let assetDataEncoder: IAssetDataContract;
let bridgeAddress: string;
let bridgeData: string;
let receiver: string;
let erc20Token: DummyERC20TokenContract;
let erc721Token: DummyERC721TokenContract;
let nftId: BigNumber;
let erc20AssetData: string;
let erc721AssetData: string;
let erc20BridgeAssetData: string;
before(async () => {
[receiver] = await env.getAccountAddressesAsync();
assetDataEncoder = new IAssetDataContract(constants.NULL_ADDRESS, env.provider);
forwarder = await TestForwarderContract.deployFrom0xArtifactAsync(
artifacts.TestForwarder,
env.provider,
env.txDefaults,
{ ...artifacts, ...ERC20Artifacts, ...ERC721Artifacts },
);
erc20Token = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
ERC20Artifacts.DummyERC20Token,
env.provider,
env.txDefaults,
ERC20Artifacts,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
constants.DUMMY_TOKEN_DECIMALS,
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
);
erc20AssetData = assetDataEncoder.ERC20Token(erc20Token.address).getABIEncodedTransactionData();
erc721Token = await DummyERC721TokenContract.deployFrom0xArtifactAsync(
ERC721Artifacts.DummyERC721Token,
env.provider,
env.txDefaults,
ERC721Artifacts,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
);
nftId = getRandomInteger(constants.ZERO_AMOUNT, constants.MAX_UINT256);
erc721AssetData = assetDataEncoder.ERC721Token(erc721Token.address, nftId).getABIEncodedTransactionData();
bridgeAddress = randomAddress();
bridgeData = hexRandom();
erc20BridgeAssetData = assetDataEncoder
.ERC20Bridge(erc20Token.address, bridgeAddress, bridgeData)
.getABIEncodedTransactionData();
});
describe('_areUnderlyingAssetsEqual', () => {
it('returns true if assetData1 == assetData2 are ERC20', async () => {
const result = await forwarder.areUnderlyingAssetsEqual(erc20AssetData, erc20AssetData).callAsync();
expect(result).to.be.true();
});
it('returns true if assetData1 == assetData2 are ERC20Bridge', async () => {
const result = await forwarder
.areUnderlyingAssetsEqual(erc20BridgeAssetData, erc20BridgeAssetData)
.callAsync();
expect(result).to.be.true();
});
it('returns true if assetData2 is the ERC20Bridge equivalent of assetData1', async () => {
const result = await forwarder.areUnderlyingAssetsEqual(erc20AssetData, erc20BridgeAssetData).callAsync();
expect(result).to.be.true();
});
it('returns false if assetData1 != assetData2 are ERC20', async () => {
const differentERC20AssetData = assetDataEncoder.ERC20Token(randomAddress()).getABIEncodedTransactionData();
const result = await forwarder
.areUnderlyingAssetsEqual(erc20AssetData, differentERC20AssetData)
.callAsync();
expect(result).to.be.false();
});
it('returns false if assetData1 is ERC20 and assetData2 is ERC721', async () => {
const result = await forwarder.areUnderlyingAssetsEqual(erc20AssetData, erc721AssetData).callAsync();
expect(result).to.be.false();
});
it('returns false if assetData2 is ERC20Bridge, but for a different token than assetData1', async () => {
const mismatchedErc20BridgeAssetData = assetDataEncoder
.ERC20Bridge(randomAddress(), bridgeAddress, bridgeData)
.getABIEncodedTransactionData();
const result = await forwarder
.areUnderlyingAssetsEqual(erc20AssetData, mismatchedErc20BridgeAssetData)
.callAsync();
expect(result).to.be.false();
});
it('returns false if assetData1 == assetData2 are ERC721', async () => {
const result = await forwarder.areUnderlyingAssetsEqual(erc721AssetData, erc721AssetData).callAsync();
expect(result).to.be.false();
});
});
describe('_transferAssetToSender', () => {
const TRANSFER_AMOUNT = new BigNumber(1);
before(async () => {
await erc20Token
.setBalance(forwarder.address, constants.INITIAL_ERC20_BALANCE)
.awaitTransactionSuccessAsync();
await erc721Token.mint(forwarder.address, nftId).awaitTransactionSuccessAsync();
});
it('transfers an ERC20 token given ERC20 assetData', async () => {
const txReceipt = await forwarder
.transferAssetToSender(erc20AssetData, TRANSFER_AMOUNT)
.awaitTransactionSuccessAsync({ from: receiver });
verifyEventsFromLogs<ERC20TokenTransferEventArgs>(
txReceipt.logs,
[{ _from: forwarder.address, _to: receiver, _value: TRANSFER_AMOUNT }],
ERC20TokenEvents.Transfer,
);
});
it('transfers an ERC721 token given ERC721 assetData and amount == 1', async () => {
const txReceipt = await forwarder
.transferAssetToSender(erc721AssetData, TRANSFER_AMOUNT)
.awaitTransactionSuccessAsync({ from: receiver });
verifyEventsFromLogs<ERC721TokenTransferEventArgs>(
txReceipt.logs,
[{ _from: forwarder.address, _to: receiver, _tokenId: nftId }],
ERC721TokenEvents.Transfer,
);
});
it('reverts if attempting to transfer an ERC721 token with amount != 1', async () => {
const invalidAmount = new BigNumber(2);
const tx = forwarder
.transferAssetToSender(erc721AssetData, invalidAmount)
.awaitTransactionSuccessAsync({ from: receiver });
const expectedError = new ForwarderRevertErrors.Erc721AmountMustEqualOneError(invalidAmount);
return expect(tx).to.revertWith(expectedError);
});
it('transfers an ERC20 token given ERC20Bridge assetData', async () => {
const txReceipt = await forwarder
.transferAssetToSender(erc20BridgeAssetData, TRANSFER_AMOUNT)
.awaitTransactionSuccessAsync({ from: receiver });
verifyEventsFromLogs<ERC20TokenTransferEventArgs>(
txReceipt.logs,
[{ _from: forwarder.address, _to: receiver, _value: TRANSFER_AMOUNT }],
ERC20TokenEvents.Transfer,
);
});
it('reverts if assetData is unsupported', async () => {
const randomBytes = hexRandom();
const tx = forwarder
.transferAssetToSender(randomBytes, TRANSFER_AMOUNT)
.awaitTransactionSuccessAsync({ from: receiver });
const expectedError = new ForwarderRevertErrors.UnsupportedAssetProxyError(hexSlice(randomBytes, 0, 4));
return expect(tx).to.revertWith(expectedError);
});
});
});

View File

@@ -13,3 +13,4 @@ export * from '../test/generated-wrappers/mixin_assets';
export * from '../test/generated-wrappers/mixin_exchange_wrapper';
export * from '../test/generated-wrappers/mixin_forwarder_core';
export * from '../test/generated-wrappers/mixin_weth';
export * from '../test/generated-wrappers/test_forwarder';

View File

@@ -13,7 +13,8 @@
"test/generated-artifacts/MixinAssets.json",
"test/generated-artifacts/MixinExchangeWrapper.json",
"test/generated-artifacts/MixinForwarderCore.json",
"test/generated-artifacts/MixinWeth.json"
"test/generated-artifacts/MixinWeth.json",
"test/generated-artifacts/TestForwarder.json"
],
"exclude": ["./deploy/solc/solc_bin"]
}

View File

@@ -1,4 +1,13 @@
[
{
"version": "1.0.3-beta.2",
"changes": [
{
"note": "Forwader <> ERC20Bridge integration tests",
"pr": 2356
}
]
},
{
"version": "1.0.3-beta.1",
"changes": [

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/interfaces/IEth2Dai.sol";
import "@0x/contracts-erc20/contracts/test/DummyERC20Token.sol";
contract TestEth2Dai is
IEth2Dai
{
uint256 private _excessBuyAmount;
function setExcessBuyAmount(uint256 amount)
external
{
_excessBuyAmount = amount;
}
function sellAllAmount(
address sellTokenAddress,
uint256 sellTokenAmount,
address buyTokenAddress,
uint256 minimumFillAmount
)
external
returns (uint256 fillAmount)
{
DummyERC20Token(sellTokenAddress).transferFrom(
msg.sender,
address(this),
sellTokenAmount
);
DummyERC20Token buyToken = DummyERC20Token(buyTokenAddress);
buyToken.mint(minimumFillAmount + _excessBuyAmount);
buyToken.transfer(
msg.sender,
minimumFillAmount + _excessBuyAmount
);
return minimumFillAmount + _excessBuyAmount;
}
}

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,109 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IUniswapExchange.sol";
import "@0x/contracts-erc20/contracts/test/DummyERC20Token.sol";
contract TestUniswapExchange is
IUniswapExchange
{
DummyERC20Token public token;
uint256 private _excessBuyAmount;
constructor(address _tokenAddress) public {
token = DummyERC20Token(_tokenAddress);
}
// solhint-disable no-empty-blocks
/// @dev Used to receive ETH for testing.
function topUpEth()
external
payable
{}
function setExcessBuyAmount(uint256 amount)
external
{
_excessBuyAmount = amount;
}
function ethToTokenTransferInput(
uint256 minTokensBought,
uint256, /* deadline */
address recipient
)
external
payable
returns (uint256 tokensBought)
{
token.mint(minTokensBought + _excessBuyAmount);
token.transfer(recipient, minTokensBought + _excessBuyAmount);
return minTokensBought + _excessBuyAmount;
}
function tokenToEthSwapInput(
uint256 tokensSold,
uint256 minEthBought,
uint256 /* deadline */
)
external
returns (uint256 ethBought)
{
token.transferFrom(
msg.sender,
address(this),
tokensSold
);
msg.sender.transfer(minEthBought + _excessBuyAmount);
return minEthBought + _excessBuyAmount;
}
function tokenToTokenTransferInput(
uint256 tokensSold,
uint256 minTokensBought,
uint256, /* minEthBought */
uint256, /* deadline */
address recipient,
address toTokenAddress
)
external
returns (uint256 tokensBought)
{
token.transferFrom(
msg.sender,
address(this),
tokensSold
);
DummyERC20Token toToken = DummyERC20Token(toTokenAddress);
toToken.mint(minTokensBought + _excessBuyAmount);
toToken.transfer(recipient, minTokensBought + _excessBuyAmount);
return minTokensBought + _excessBuyAmount;
}
function toTokenAddress()
external
view
returns (address _tokenAddress)
{
return address(token);
}
}

View File

@@ -0,0 +1,52 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-asset-proxy/contracts/src/bridges/UniswapBridge.sol";
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IUniswapExchangeFactory.sol";
contract TestUniswapExchangeFactory is
IUniswapExchangeFactory
{
// Token address to UniswapExchange address.
mapping (address => address) private _testExchanges;
/// @dev Create a token and exchange (if they don't exist) for a new token
/// and sets the exchange revert and fill behavior.
/// @param tokenAddress The token address.
function addExchange(
address tokenAddress,
address exchangeAddress
)
external
{
_testExchanges[tokenAddress] = exchangeAddress;
}
/// @dev `IUniswapExchangeFactory.getExchange`
function getExchange(address tokenAddress)
external
view
returns (address)
{
return _testExchanges[tokenAddress];
}
}

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,31 @@
import { artifacts as ERC20Artifacts } from '@0x/contracts-erc20';
import { BlockchainTestsEnvironment } from '@0x/contracts-test-utils';
import { artifacts } from '../artifacts';
import { DeploymentManager } from '../framework/deployment_manager';
import { TestEth2DaiBridgeContract, TestEth2DaiContract } from '../wrappers';
/**
* Deploys test Eth2Dai exchange and bridge contracts configured to work alongside the provided `deployment`.
*/
export async function deployEth2DaiBridgeAsync(
deployment: DeploymentManager,
environment: BlockchainTestsEnvironment,
): Promise<[TestEth2DaiBridgeContract, TestEth2DaiContract]> {
const eth2Dai = await TestEth2DaiContract.deployFrom0xArtifactAsync(
artifacts.TestEth2Dai,
environment.provider,
deployment.txDefaults,
artifacts,
);
const eth2DaiBridge = await TestEth2DaiBridgeContract.deployFrom0xArtifactAsync(
artifacts.TestEth2DaiBridge,
environment.provider,
deployment.txDefaults,
{ ...ERC20Artifacts, ...artifacts },
eth2Dai.address,
);
return [eth2DaiBridge, eth2Dai];
}

View File

@@ -0,0 +1,51 @@
import { artifacts as ERC20Artifacts } from '@0x/contracts-erc20';
import { BlockchainTestsEnvironment } from '@0x/contracts-test-utils';
import { artifacts } from '../artifacts';
import { DeploymentManager } from '../framework/deployment_manager';
import {
TestUniswapBridgeContract,
TestUniswapExchangeContract,
TestUniswapExchangeFactoryContract,
} from '../wrappers';
/**
* Deploys test Uniswap exchanges for the given tokens, a test UniswapExchangeFactory, and a test
* bridge contract configured to work alongside the provided `deployment`.
*/
export async function deployUniswapBridgeAsync(
deployment: DeploymentManager,
environment: BlockchainTestsEnvironment,
tokenAddresses: string[],
): Promise<[TestUniswapBridgeContract, TestUniswapExchangeContract[], TestUniswapExchangeFactoryContract]> {
const uniswapExchangeFactory = await TestUniswapExchangeFactoryContract.deployFrom0xArtifactAsync(
artifacts.TestUniswapExchangeFactory,
environment.provider,
deployment.txDefaults,
artifacts,
);
const uniswapExchanges = [];
for (const tokenAddress of tokenAddresses) {
const uniswapExchange = await TestUniswapExchangeContract.deployFrom0xArtifactAsync(
artifacts.TestUniswapExchange,
environment.provider,
deployment.txDefaults,
artifacts,
tokenAddress,
);
await uniswapExchangeFactory.addExchange(tokenAddress, uniswapExchange.address).awaitTransactionSuccessAsync();
uniswapExchanges.push(uniswapExchange);
}
const uniswapBridge = await TestUniswapBridgeContract.deployFrom0xArtifactAsync(
artifacts.TestUniswapBridge,
environment.provider,
deployment.txDefaults,
{ ...ERC20Artifacts, ...artifacts },
deployment.tokens.weth.address,
uniswapExchangeFactory.address,
);
return [uniswapBridge, uniswapExchanges, uniswapExchangeFactory];
}

View File

@@ -1,5 +1,4 @@
import { CoordinatorContract, CoordinatorRevertErrors, SignedCoordinatorApproval } from '@0x/contracts-coordinator';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import {
ExchangeCancelEventArgs,
ExchangeCancelUpToEventArgs,
@@ -15,7 +14,6 @@ import {
hexConcat,
hexSlice,
orderHashUtils,
provider,
transactionHashUtils,
verifyEvents,
} from '@0x/contracts-test-utils';
@@ -38,7 +36,6 @@ blockchainTests.resets('Coordinator integration tests', env => {
let deployment: DeploymentManager;
let coordinator: CoordinatorContract;
let balanceStore: BlockchainBalanceStore;
let devUtils: DevUtilsContract;
let maker: Maker;
let taker: Actor;
@@ -51,7 +48,6 @@ blockchainTests.resets('Coordinator integration tests', env => {
numErc1155TokensToDeploy: 0,
});
coordinator = await deployCoordinatorAsync(deployment, env);
devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider);
const [makerToken, takerToken, makerFeeToken, takerFeeToken] = deployment.tokens.erc20;
@@ -109,7 +105,7 @@ blockchainTests.resets('Coordinator integration tests', env => {
msgValue?: BigNumber,
): Promise<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

@@ -221,7 +221,6 @@ blockchainTests.resets('Exchange core', () => {
};
fillOrderWrapper = new FillOrderWrapper(
exchange,
devUtils,
{ makerAddress, takerAddress, feeRecipientAddress },
tokenContracts,
tokenIds,

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

@@ -1,4 +1,3 @@
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { ExchangeContract } from '@0x/contracts-exchange';
import { ReferenceFunctions as LibReferenceFunctions } from '@0x/contracts-exchange-libs';
import {
@@ -56,65 +55,6 @@ export class FillOrderWrapper {
return events.map(event => _.pick(event, fieldsOfInterest)) as FillEventArgs[];
}
/**
* Locally simulates filling an order.
* @param txReceipt Transaction receipt from the actual fill, needed to update eth balance
* @param signedOrder The order being filled.
* @param takerAddress Address of taker (the address who matched the two orders)
* @param opts Optionally specifies the amount to fill.
* @param initBalanceStore Account balances prior to the fill.
* @return The expected account balances, fill results, and fill events.
*/
public async simulateFillOrderAsync(
txReceipt: TransactionReceiptWithDecodedLogs,
signedOrder: SignedOrder,
takerAddress: string,
initBalanceStore: BalanceStore,
opts: { takerAssetFillAmount?: BigNumber } = {},
): Promise<[FillResults, FillEventArgs, BalanceStore]> {
const balanceStore = LocalBalanceStore.create(this._devUtils, initBalanceStore);
const takerAssetFillAmount =
opts.takerAssetFillAmount !== undefined ? opts.takerAssetFillAmount : signedOrder.takerAssetAmount;
// TODO(jalextowle): Change this if the integration tests take protocol fees into account.
const fillResults = LibReferenceFunctions.calculateFillResults(
signedOrder,
takerAssetFillAmount,
constants.ZERO_AMOUNT,
constants.ZERO_AMOUNT,
);
const fillEvent = FillOrderWrapper.simulateFillEvent(signedOrder, takerAddress, fillResults);
// Taker -> Maker
await balanceStore.transferAssetAsync(
takerAddress,
signedOrder.makerAddress,
fillResults.takerAssetFilledAmount,
signedOrder.takerAssetData,
);
// Maker -> Taker
await balanceStore.transferAssetAsync(
signedOrder.makerAddress,
takerAddress,
fillResults.makerAssetFilledAmount,
signedOrder.makerAssetData,
);
// Taker -> Fee Recipient
await balanceStore.transferAssetAsync(
takerAddress,
signedOrder.feeRecipientAddress,
fillResults.takerFeePaid,
signedOrder.takerFeeAssetData,
);
// Maker -> Fee Recipient
await balanceStore.transferAssetAsync(
signedOrder.makerAddress,
signedOrder.feeRecipientAddress,
fillResults.makerFeePaid,
signedOrder.makerFeeAssetData,
);
balanceStore.burnGas(txReceipt.from, constants.DEFAULT_GAS_PRICE * txReceipt.gasUsed);
return [fillResults, fillEvent, balanceStore];
}
/**
* Constructor.
* @param exchangeContract Instance of the deployed exchange contract.
@@ -124,7 +64,6 @@ export class FillOrderWrapper {
*/
public constructor(
private readonly _exchange: ExchangeContract,
private readonly _devUtils: DevUtilsContract,
tokenOwnersByName: TokenOwnersByName,
tokenContractsByName: Partial<TokenContractsByName>,
tokenIds: Partial<TokenIds>,
@@ -160,11 +99,13 @@ export class FillOrderWrapper {
await this._assertOrderStateAsync(signedOrder, initTakerAssetFilledAmount);
// Simulate and execute fill then assert outputs
const [fillResults, fillEvent, txReceipt] = await this._fillOrderAsync(signedOrder, from, opts);
const [
simulatedFillResults,
simulatedFillEvent,
simulatedFinalBalanceStore,
] = await this.simulateFillOrderAsync(txReceipt, signedOrder, from, this._blockchainBalanceStore, opts);
const [simulatedFillResults, simulatedFillEvent, simulatedFinalBalanceStore] = await simulateFillOrderAsync(
txReceipt,
signedOrder,
from,
this._blockchainBalanceStore,
opts,
);
// Assert state transition
expect(simulatedFillResults, 'Fill Results').to.be.deep.equal(fillResults);
expect(simulatedFillEvent, 'Fill Events').to.be.deep.equal(fillEvent);
@@ -218,3 +159,62 @@ export class FillOrderWrapper {
expect(actualStatus, 'order status').to.equal(expectedStatus);
}
}
/**
* Locally simulates filling an order.
* @param txReceipt Transaction receipt from the actual fill, needed to update eth balance
* @param signedOrder The order being filled.
* @param takerAddress Address of taker (the address who matched the two orders)
* @param opts Optionally specifies the amount to fill.
* @param initBalanceStore Account balances prior to the fill.
* @return The expected account balances, fill results, and fill events.
*/
async function simulateFillOrderAsync(
txReceipt: TransactionReceiptWithDecodedLogs,
signedOrder: SignedOrder,
takerAddress: string,
initBalanceStore: BalanceStore,
opts: { takerAssetFillAmount?: BigNumber } = {},
): Promise<[FillResults, FillEventArgs, BalanceStore]> {
const balanceStore = LocalBalanceStore.create(initBalanceStore);
const takerAssetFillAmount =
opts.takerAssetFillAmount !== undefined ? opts.takerAssetFillAmount : signedOrder.takerAssetAmount;
// TODO(jalextowle): Change this if the integration tests take protocol fees into account.
const fillResults = LibReferenceFunctions.calculateFillResults(
signedOrder,
takerAssetFillAmount,
constants.ZERO_AMOUNT,
constants.ZERO_AMOUNT,
);
const fillEvent = FillOrderWrapper.simulateFillEvent(signedOrder, takerAddress, fillResults);
// Taker -> Maker
await balanceStore.transferAssetAsync(
takerAddress,
signedOrder.makerAddress,
fillResults.takerAssetFilledAmount,
signedOrder.takerAssetData,
);
// Maker -> Taker
await balanceStore.transferAssetAsync(
signedOrder.makerAddress,
takerAddress,
fillResults.makerAssetFilledAmount,
signedOrder.makerAssetData,
);
// Taker -> Fee Recipient
await balanceStore.transferAssetAsync(
takerAddress,
signedOrder.feeRecipientAddress,
fillResults.takerFeePaid,
signedOrder.takerFeeAssetData,
);
// Maker -> Fee Recipient
await balanceStore.transferAssetAsync(
signedOrder.makerAddress,
signedOrder.feeRecipientAddress,
fillResults.makerFeePaid,
signedOrder.makerFeeAssetData,
);
balanceStore.burnGas(txReceipt.from, constants.DEFAULT_GAS_PRICE * txReceipt.gasUsed);
return [fillResults, fillEvent, balanceStore];
}

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

@@ -168,7 +168,7 @@ export class MatchOrderTester {
// Update the blockchain balance store and create a new local balance store
// with the same initial balances.
await this._blockchainBalanceStore.updateBalancesAsync();
const localBalanceStore = LocalBalanceStore.create(this._deployment.devUtils, this._blockchainBalanceStore);
const localBalanceStore = LocalBalanceStore.create(this._blockchainBalanceStore);
// Execute `batchMatchOrders()`
let actualBatchMatchResults;
@@ -253,7 +253,7 @@ export class MatchOrderTester {
// Update the blockchain balance store and create a new local balance store
// with the same initial balances.
await this._blockchainBalanceStore.updateBalancesAsync();
const localBalanceStore = LocalBalanceStore.create(this._deployment.devUtils, this._blockchainBalanceStore);
const localBalanceStore = LocalBalanceStore.create(this._blockchainBalanceStore);
// Execute `matchOrders()`
let actualMatchResults;

View File

@@ -0,0 +1,378 @@
import { IAssetDataContract } from '@0x/contracts-asset-proxy';
import { DummyERC721TokenContract } from '@0x/contracts-erc721';
import { ForwarderContract, ForwarderRevertErrors } from '@0x/contracts-exchange-forwarder';
import {
blockchainTests,
constants,
getLatestBlockTimestampAsync,
hexConcat,
toBaseUnitAmount,
} from '@0x/contracts-test-utils';
import { generatePseudoRandomSalt } from '@0x/order-utils';
import { SignatureType, SignedOrder } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils';
import { deployEth2DaiBridgeAsync } from '../bridges/deploy_eth2dai_bridge';
import { deployUniswapBridgeAsync } from '../bridges/deploy_uniswap_bridge';
import { Actor } from '../framework/actors/base';
import { FeeRecipient } from '../framework/actors/fee_recipient';
import { Maker } from '../framework/actors/maker';
import { Taker } from '../framework/actors/taker';
import { actorAddressesByName } from '../framework/actors/utils';
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
import { DeploymentManager } from '../framework/deployment_manager';
import { TestEth2DaiContract, TestUniswapExchangeContract } from '../wrappers';
import { deployForwarderAsync } from './deploy_forwarder';
import { ForwarderTestFactory } from './forwarder_test_factory';
blockchainTests.resets('Forwarder <> ERC20Bridge integration tests', env => {
let deployment: DeploymentManager;
let balanceStore: BlockchainBalanceStore;
let testFactory: ForwarderTestFactory;
let forwarder: ForwarderContract;
let assetDataEncoder: IAssetDataContract;
let eth2Dai: TestEth2DaiContract;
let uniswapExchange: TestUniswapExchangeContract;
let erc721Token: DummyERC721TokenContract;
let nftId: BigNumber;
let makerTokenAssetData: string;
let makerFeeTokenAssetData: string;
let eth2DaiBridgeAssetData: string;
let uniswapBridgeAssetData: string;
let maker: Maker;
let taker: Taker;
let orderFeeRecipient: FeeRecipient;
let forwarderFeeRecipient: FeeRecipient;
let eth2DaiBridgeOrder: SignedOrder;
let uniswapBridgeOrder: SignedOrder;
before(async () => {
assetDataEncoder = new IAssetDataContract(constants.NULL_ADDRESS, env.provider);
deployment = await DeploymentManager.deployAsync(env, {
numErc20TokensToDeploy: 2,
numErc721TokensToDeploy: 1,
numErc1155TokensToDeploy: 0,
});
const [makerToken, makerFeeToken] = deployment.tokens.erc20;
[erc721Token] = deployment.tokens.erc721;
forwarder = await deployForwarderAsync(deployment, env);
const eth2DaiContracts = await deployEth2DaiBridgeAsync(deployment, env);
const [eth2DaiBridge] = eth2DaiContracts;
[, eth2Dai] = eth2DaiContracts;
const uniswapContracts = await deployUniswapBridgeAsync(deployment, env, [makerToken.address]);
const [uniswapBridge] = uniswapContracts;
[, [uniswapExchange]] = uniswapContracts;
makerTokenAssetData = assetDataEncoder.ERC20Token(makerToken.address).getABIEncodedTransactionData();
makerFeeTokenAssetData = assetDataEncoder.ERC20Token(makerFeeToken.address).getABIEncodedTransactionData();
const wethAssetData = assetDataEncoder
.ERC20Token(deployment.tokens.weth.address)
.getABIEncodedTransactionData();
const bridgeDataEncoder = AbiEncoder.create([{ name: 'fromTokenAddress', type: 'address' }]);
const bridgeData = bridgeDataEncoder.encode([deployment.tokens.weth.address]);
eth2DaiBridgeAssetData = assetDataEncoder
.ERC20Bridge(makerToken.address, eth2DaiBridge.address, bridgeData)
.getABIEncodedTransactionData();
uniswapBridgeAssetData = assetDataEncoder
.ERC20Bridge(makerToken.address, uniswapBridge.address, bridgeData)
.getABIEncodedTransactionData();
taker = new Taker({ name: 'Taker', deployment });
orderFeeRecipient = new FeeRecipient({
name: 'Order fee recipient',
deployment,
});
forwarderFeeRecipient = new FeeRecipient({
name: 'Forwarder fee recipient',
deployment,
});
const fifteenMinutesInSeconds = 15 * 60;
const currentBlockTimestamp = await getLatestBlockTimestampAsync();
const orderDefaults = {
chainId: deployment.chainId,
exchangeAddress: deployment.exchange.address,
takerAddress: constants.NULL_ADDRESS,
feeRecipientAddress: orderFeeRecipient.address,
senderAddress: constants.NULL_ADDRESS,
makerAssetAmount: toBaseUnitAmount(2),
takerAssetAmount: toBaseUnitAmount(1),
takerAssetData: wethAssetData,
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
makerFeeAssetData: makerFeeTokenAssetData,
takerFeeAssetData: wethAssetData,
expirationTimeSeconds: new BigNumber(currentBlockTimestamp).plus(fifteenMinutesInSeconds),
salt: generatePseudoRandomSalt(),
signature: hexConcat(SignatureType.Wallet),
};
eth2DaiBridgeOrder = {
...orderDefaults,
makerAddress: eth2DaiBridge.address,
makerAssetData: eth2DaiBridgeAssetData,
};
uniswapBridgeOrder = {
...orderDefaults,
makerAddress: uniswapBridge.address,
makerAssetData: uniswapBridgeAssetData,
};
maker = new Maker({
name: 'Maker',
deployment,
orderConfig: { ...orderDefaults, makerFee: toBaseUnitAmount(0.01) },
});
await maker.configureERC20TokenAsync(makerToken);
await maker.configureERC20TokenAsync(makerFeeToken);
await forwarder.approveMakerAssetProxy(makerTokenAssetData).awaitTransactionSuccessAsync();
[nftId] = await maker.configureERC721TokenAsync(erc721Token);
// We need to top up the TestUniswapExchange with some ETH so that it can perform tokenToEthSwapInput
await uniswapExchange.topUpEth().awaitTransactionSuccessAsync({
from: forwarderFeeRecipient.address,
value: constants.ONE_ETHER.times(10),
});
const tokenOwners = {
...actorAddressesByName([maker, taker, orderFeeRecipient, forwarderFeeRecipient]),
Forwarder: forwarder.address,
StakingProxy: deployment.staking.stakingProxy.address,
};
const tokenContracts = {
erc20: { makerToken, makerFeeToken, wETH: deployment.tokens.weth },
erc721: { erc721Token },
};
const tokenIds = { erc721: { [erc721Token.address]: [nftId] } };
balanceStore = new BlockchainBalanceStore(tokenOwners, tokenContracts, tokenIds);
testFactory = new ForwarderTestFactory(forwarder, deployment, balanceStore, taker, forwarderFeeRecipient);
});
after(async () => {
Actor.count = 0;
});
describe('marketSellOrdersWithEth', () => {
it('should fully fill a single Eth2DaiBridge order without a taker fee', async () => {
await testFactory.marketSellTestAsync([eth2DaiBridgeOrder], 1);
});
it('should partially fill a single Eth2DaiBridge order without a taker fee', async () => {
await testFactory.marketSellTestAsync([eth2DaiBridgeOrder], 0.34);
});
it('should correctly handle excess maker asset acquired from Eth2Dai', async () => {
const bridgeExcessBuyAmount = new BigNumber(1);
await eth2Dai.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync();
await testFactory.marketSellTestAsync([eth2DaiBridgeOrder], 0.34, { bridgeExcessBuyAmount });
});
it('should fill a single Eth2DaiBridge order with a WETH taker fee', async () => {
const order = {
...eth2DaiBridgeOrder,
takerFee: toBaseUnitAmount(0.01),
};
await testFactory.marketSellTestAsync([order], 0.78);
});
it('should fill a single Eth2DaiBridge order with a percentage taker fee', async () => {
const order = {
...eth2DaiBridgeOrder,
takerFee: toBaseUnitAmount(0.01),
takerFeeAssetData: makerTokenAssetData,
};
await testFactory.marketSellTestAsync([order], 0.78);
});
it('should fill an Eth2DaiBridge order along with non-bridge orders, with an affiliate fee', async () => {
const orders = [
// ERC721 order
await maker.signOrderAsync({
makerAssetAmount: new BigNumber(1),
makerAssetData: assetDataEncoder
.ERC721Token(erc721Token.address, nftId)
.getABIEncodedTransactionData(),
takerFee: toBaseUnitAmount(0.01),
}),
eth2DaiBridgeOrder,
await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20
];
await testFactory.marketSellTestAsync(orders, 2.56, { forwarderFeePercentage: 1 });
});
it('should fully fill a single UniswapBridge order without a taker fee', async () => {
await testFactory.marketSellTestAsync([uniswapBridgeOrder], 1);
});
it('should partially fill a single UniswapBridge order without a taker fee', async () => {
await testFactory.marketSellTestAsync([uniswapBridgeOrder], 0.34);
});
it('should correctly handle excess maker asset acquired from Uniswap', async () => {
const bridgeExcessBuyAmount = new BigNumber(1);
await uniswapExchange.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync();
await testFactory.marketSellTestAsync([uniswapBridgeOrder], 0.34, { bridgeExcessBuyAmount });
});
it('should fill a single UniswapBridge order with a WETH taker fee', async () => {
const order = {
...uniswapBridgeOrder,
takerFee: toBaseUnitAmount(0.01),
};
await testFactory.marketSellTestAsync([order], 0.78);
});
it('should fill a single UniswapBridge order with a percentage taker fee', async () => {
const order = {
...uniswapBridgeOrder,
takerFee: toBaseUnitAmount(0.01),
takerFeeAssetData: makerTokenAssetData,
};
await testFactory.marketSellTestAsync([order], 0.78);
});
it('should fill an UniswapBridge order along with non-bridge orders', async () => {
const orders = [
// ERC721 order
await maker.signOrderAsync({
makerAssetAmount: new BigNumber(1),
makerAssetData: assetDataEncoder
.ERC721Token(erc721Token.address, nftId)
.getABIEncodedTransactionData(),
takerFee: toBaseUnitAmount(0.01),
}),
uniswapBridgeOrder,
await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20
];
await testFactory.marketSellTestAsync(orders, 2.56, { forwarderFeePercentage: 1 });
});
it('should fill multiple bridge orders', async () => {
await testFactory.marketSellTestAsync([eth2DaiBridgeOrder, uniswapBridgeOrder], 1.23);
});
it('should revert if the takerFee is denominated in a different token', async () => {
const order = {
...eth2DaiBridgeOrder,
takerFee: toBaseUnitAmount(0.01),
takerFeeAssetData: makerFeeTokenAssetData,
};
const expectedError = new ForwarderRevertErrors.UnsupportedFeeError(makerFeeTokenAssetData);
await testFactory.marketSellTestAsync([order], 1.23, { revertError: expectedError });
});
});
describe('marketBuyOrdersWithEth', () => {
it('should fully fill a single Eth2DaiBridge order without a taker fee', async () => {
await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 1);
});
it('should partially fill a single Eth2DaiBridge order without a taker fee', async () => {
await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 0.34);
});
it('should return excess ETH', async () => {
await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 1, { ethValueAdjustment: 1 });
});
it('should correctly handle excess maker asset acquired from Eth2Dai', async () => {
const bridgeExcessBuyAmount = new BigNumber(1);
await eth2Dai.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync();
await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 0.34, { bridgeExcessBuyAmount });
});
it('should fill a single Eth2DaiBridge order with a WETH taker fee', async () => {
const order = {
...eth2DaiBridgeOrder,
takerFee: toBaseUnitAmount(0.01),
};
await testFactory.marketBuyTestAsync([order], 0.78);
});
it('should fill a single Eth2DaiBridge order with a percentage taker fee', async () => {
const order = {
...eth2DaiBridgeOrder,
takerFee: toBaseUnitAmount(0.01),
takerFeeAssetData: makerTokenAssetData,
};
await testFactory.marketBuyTestAsync([order], 0.78);
});
it('should fill an Eth2DaiBridge order along with non-bridge orders, with an affiliate fee', async () => {
const orders = [
// ERC721 order
await maker.signOrderAsync({
makerAssetAmount: new BigNumber(1),
makerAssetData: assetDataEncoder
.ERC721Token(erc721Token.address, nftId)
.getABIEncodedTransactionData(),
takerFee: toBaseUnitAmount(0.01),
}),
eth2DaiBridgeOrder,
await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20
];
await testFactory.marketBuyTestAsync(orders, 2.56, { forwarderFeePercentage: 1 });
});
it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount (Eth2Dai)', async () => {
const expectedError = new ForwarderRevertErrors.CompleteBuyFailedError(
eth2DaiBridgeOrder.makerAssetAmount.times(0.5),
constants.ZERO_AMOUNT,
);
await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder], 0.5, {
ethValueAdjustment: -2,
revertError: expectedError,
});
});
it('should fully fill a single UniswapBridge order without a taker fee', async () => {
await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 1);
});
it('should partially fill a single UniswapBridge order without a taker fee', async () => {
await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 0.34);
});
it('should correctly handle excess maker asset acquired from Uniswap', async () => {
const bridgeExcessBuyAmount = new BigNumber(1);
await uniswapExchange.setExcessBuyAmount(bridgeExcessBuyAmount).awaitTransactionSuccessAsync();
await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 0.34, { bridgeExcessBuyAmount });
});
it('should fill a single UniswapBridge order with a WETH taker fee', async () => {
const order = {
...uniswapBridgeOrder,
takerFee: toBaseUnitAmount(0.01),
};
await testFactory.marketBuyTestAsync([order], 0.78);
});
it('should fill a single UniswapBridge order with a percentage taker fee', async () => {
const order = {
...uniswapBridgeOrder,
takerFee: toBaseUnitAmount(0.01),
takerFeeAssetData: makerTokenAssetData,
};
await testFactory.marketBuyTestAsync([order], 0.78);
});
it('should fill an UniswapBridge order along with non-bridge orders', async () => {
const orders = [
// ERC721 order
await maker.signOrderAsync({
makerAssetAmount: new BigNumber(1),
makerAssetData: assetDataEncoder
.ERC721Token(erc721Token.address, nftId)
.getABIEncodedTransactionData(),
takerFee: toBaseUnitAmount(0.01),
}),
uniswapBridgeOrder,
await maker.signOrderAsync({ makerAssetData: makerTokenAssetData }), // Non-bridge order of the same ERC20
];
await testFactory.marketBuyTestAsync(orders, 2.56, { forwarderFeePercentage: 1 });
});
it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount (Uniswap)', async () => {
const expectedError = new ForwarderRevertErrors.CompleteBuyFailedError(
uniswapBridgeOrder.makerAssetAmount.times(0.5),
constants.ZERO_AMOUNT,
);
await testFactory.marketBuyTestAsync([uniswapBridgeOrder], 0.5, {
ethValueAdjustment: -2,
revertError: expectedError,
});
});
it('should fill multiple bridge orders', async () => {
await testFactory.marketBuyTestAsync([eth2DaiBridgeOrder, uniswapBridgeOrder], 1.23);
});
it('should revert if the takerFee is denominated in a different token', async () => {
const order = {
...eth2DaiBridgeOrder,
takerFee: toBaseUnitAmount(0.01),
takerFeeAssetData: makerFeeTokenAssetData,
};
const expectedError = new ForwarderRevertErrors.UnsupportedFeeError(makerFeeTokenAssetData);
await testFactory.marketBuyTestAsync([order], 1.23, { revertError: expectedError });
});
});
});
// tslint:disable:max-file-line-count

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

@@ -1,4 +1,3 @@
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { DummyERC721TokenContract } from '@0x/contracts-erc721';
import { artifacts as exchangeArtifacts, ExchangeContract } from '@0x/contracts-exchange';
@@ -9,7 +8,6 @@ import {
expect,
getLatestBlockTimestampAsync,
getPercentageOfValue,
provider,
toBaseUnitAmount,
} from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
@@ -26,8 +24,6 @@ import { DeploymentManager } from '../framework/deployment_manager';
import { deployForwarderAsync } from './deploy_forwarder';
import { ForwarderTestFactory } from './forwarder_test_factory';
const devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider);
blockchainTests('Forwarder integration tests', env => {
let deployment: DeploymentManager;
let forwarder: ForwarderContract;
@@ -106,16 +102,7 @@ blockchainTests('Forwarder integration tests', env => {
const tokenIds = { erc721: { [erc721Token.address]: [nftId] } };
balanceStore = new BlockchainBalanceStore(tokenOwners, tokenContracts, tokenIds);
testFactory = new ForwarderTestFactory(
forwarder,
deployment,
balanceStore,
maker,
taker,
orderFeeRecipient,
forwarderFeeRecipient,
devUtils,
);
testFactory = new ForwarderTestFactory(forwarder, deployment, balanceStore, taker, forwarderFeeRecipient);
});
after(async () => {
@@ -138,7 +125,7 @@ blockchainTests('Forwarder integration tests', env => {
env.txDefaults,
{},
exchange.address,
wethAssetData,
deployment.tokens.weth.address,
);
await expect(deployForwarder).to.revertWith(new ForwarderRevertErrors.UnregisteredAssetProxyError());
});
@@ -202,7 +189,7 @@ blockchainTests('Forwarder integration tests', env => {
from: taker.address,
});
const expectedBalances = LocalBalanceStore.create(devUtils, balanceStore);
const expectedBalances = LocalBalanceStore.create(balanceStore);
expectedBalances.burnGas(tx.from, DeploymentManager.gasPrice.times(tx.gasUsed));
// Verify balances
@@ -521,7 +508,7 @@ blockchainTests('Forwarder integration tests', env => {
});
// Compute expected balances
const expectedBalances = LocalBalanceStore.create(devUtils, balanceStore);
const expectedBalances = LocalBalanceStore.create(balanceStore);
await expectedBalances.transferAssetAsync(
maker.address,
taker.address,
@@ -578,7 +565,7 @@ blockchainTests('Forwarder integration tests', env => {
});
// Compute expected balances
const expectedBalances = LocalBalanceStore.create(devUtils, balanceStore);
const expectedBalances = LocalBalanceStore.create(balanceStore);
await expectedBalances.transferAssetAsync(
maker.address,
taker.address,

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,24 +27,44 @@ interface ForwarderFillState {
}
interface MarketSellOptions {
forwarderFeePercentage: BigNumber;
forwarderFeePercentage: Numberish;
revertError: RevertError;
bridgeExcessBuyAmount: BigNumber;
}
interface MarketBuyOptions extends MarketSellOptions {
ethValueAdjustment: number; // Used to provided insufficient/excess ETH
}
function areUnderlyingAssetsEqual(assetData1: string, assetData2: string): boolean {
const assetProxyId1 = hexSlice(assetData1, 0, 4);
const assetProxyId2 = hexSlice(assetData2, 0, 4);
if (
(assetProxyId1 === AssetProxyId.ERC20 || assetProxyId1 === AssetProxyId.ERC20Bridge) &&
(assetProxyId2 === AssetProxyId.ERC20 || assetProxyId2 === AssetProxyId.ERC20Bridge)
) {
const assetDataDecoder = new IAssetDataContract(constants.NULL_ADDRESS, provider);
const tokenAddress1 =
assetProxyId1 === AssetProxyId.ERC20
? assetDataDecoder.getABIDecodedTransactionData<string>('ERC20Token', assetData1)
: assetDataDecoder.getABIDecodedTransactionData<[string]>('ERC20Bridge', assetData1)[0];
const tokenAddress2 =
assetProxyId2 === AssetProxyId.ERC20
? assetDataDecoder.getABIDecodedTransactionData<string>('ERC20Token', assetData2)
: assetDataDecoder.getABIDecodedTransactionData<[string]>('ERC20Bridge', assetData2)[0];
return tokenAddress2 === tokenAddress1;
} else {
return false;
}
}
export class ForwarderTestFactory {
constructor(
private readonly _forwarder: ForwarderContract,
private readonly _deployment: DeploymentManager,
private readonly _balanceStore: BlockchainBalanceStore,
private readonly _maker: Maker,
private readonly _taker: Taker,
private readonly _orderFeeRecipient: FeeRecipient,
private readonly _forwarderFeeRecipient: FeeRecipient,
private readonly _devUtils: DevUtilsContract,
) {}
public async marketBuyTestAsync(
@@ -69,7 +96,7 @@ export class ForwarderTestFactory {
const tx = this._forwarder
.marketBuyOrdersWithEth(
orders,
makerAssetAcquiredAmount,
makerAssetAcquiredAmount.minus(options.bridgeExcessBuyAmount || 0),
orders.map(signedOrder => signedOrder.signature),
feePercentage,
this._forwarderFeeRecipient.address,
@@ -164,7 +191,7 @@ export class ForwarderTestFactory {
options: Partial<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,
@@ -185,6 +212,7 @@ export class ForwarderTestFactory {
order,
ordersInfoBefore[i].orderTakerAssetFilledAmount,
Math.min(remainingOrdersToFill, 1),
options.bridgeExcessBuyAmount || constants.ZERO_AMOUNT,
);
remainingOrdersToFill = Math.max(remainingOrdersToFill - 1, 0);
@@ -209,6 +237,7 @@ export class ForwarderTestFactory {
order: SignedOrder,
takerAssetFilled: BigNumber,
fillFraction: number,
bridgeExcessBuyAmount: BigNumber,
): Promise<ForwarderFillState> {
let { makerAssetAmount, takerAssetAmount, makerFee, takerFee } = order;
makerAssetAmount = makerAssetAmount.times(fillFraction).integerValue(BigNumber.ROUND_CEIL);
@@ -228,9 +257,10 @@ export class ForwarderTestFactory {
const takerFeeFilled = takerAssetFilled.times(order.takerFee).dividedToIntegerBy(order.takerAssetAmount);
takerFee = BigNumber.max(takerFee.minus(takerFeeFilled), 0);
makerAssetAmount = makerAssetAmount.plus(bridgeExcessBuyAmount);
let wethSpentAmount = takerAssetAmount.plus(DeploymentManager.protocolFee);
let makerAssetAcquiredAmount = makerAssetAmount;
if (order.takerFeeAssetData === order.makerAssetData) {
if (areUnderlyingAssetsEqual(order.takerFeeAssetData, order.makerAssetData)) {
makerAssetAcquiredAmount = makerAssetAcquiredAmount.minus(takerFee);
} else if (order.takerFeeAssetData === order.takerAssetData) {
wethSpentAmount = wethSpentAmount.plus(takerFee);
@@ -244,29 +274,29 @@ export class ForwarderTestFactory {
// Maker -> Forwarder
await balances.transferAssetAsync(
this._maker.address,
order.makerAddress,
this._forwarder.address,
makerAssetAmount,
order.makerAssetData,
);
// Maker -> Order fee recipient
await balances.transferAssetAsync(
this._maker.address,
this._orderFeeRecipient.address,
order.makerAddress,
order.feeRecipientAddress,
makerFee,
order.makerFeeAssetData,
);
// Forwarder -> Maker
await balances.transferAssetAsync(
this._forwarder.address,
this._maker.address,
order.makerAddress,
takerAssetAmount,
order.takerAssetData,
);
// Forwarder -> Order fee recipient
await balances.transferAssetAsync(
this._forwarder.address,
this._orderFeeRecipient.address,
order.feeRecipientAddress,
takerFee,
order.takerFeeAssetData,
);

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 readonly _assetDataDecoder: IAssetDataContract;
/**
* Creates a new balance store based on an existing one.
* @param sourceBalanceStore Existing balance store whose values should be copied.
*/
public static create(devUtils: DevUtilsContract, sourceBalanceStore?: BalanceStore): LocalBalanceStore {
const localBalanceStore = new LocalBalanceStore(devUtils);
public static create(sourceBalanceStore?: BalanceStore): LocalBalanceStore {
const localBalanceStore = new LocalBalanceStore();
if (sourceBalanceStore !== undefined) {
localBalanceStore.cloneFrom(sourceBalanceStore);
}
@@ -26,11 +28,11 @@ export class LocalBalanceStore extends BalanceStore {
* be initialized via `create`.
*/
protected constructor(
private readonly _devUtils: DevUtilsContract,
tokenOwnersByName: TokenOwnersByName = {},
tokenContractsByName: Partial<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"
]
}