aboutsummaryrefslogtreecommitdiffstats
path: root/packages/contracts/test
diff options
context:
space:
mode:
Diffstat (limited to 'packages/contracts/test')
-rw-r--r--packages/contracts/test/forwarder/forwarder.ts819
-rw-r--r--packages/contracts/test/utils/artifacts.ts2
-rw-r--r--packages/contracts/test/utils/erc20_wrapper.ts8
-rw-r--r--packages/contracts/test/utils/forwarder_wrapper.ts220
-rw-r--r--packages/contracts/test/utils/types.ts8
5 files changed, 1057 insertions, 0 deletions
diff --git a/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts
new file mode 100644
index 000000000..33fd6e27d
--- /dev/null
+++ b/packages/contracts/test/forwarder/forwarder.ts
@@ -0,0 +1,819 @@
+import { BlockchainLifecycle } from '@0xproject/dev-utils';
+import { assetProxyUtils } from '@0xproject/order-utils';
+import { AssetProxyId, RevertReason, SignedOrder } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as chai from 'chai';
+import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
+
+import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_e_r_c20_token';
+import { DummyERC721TokenContract } from '../../generated_contract_wrappers/dummy_e_r_c721_token';
+import { ExchangeContract } from '../../generated_contract_wrappers/exchange';
+import { ForwarderContract } from '../../generated_contract_wrappers/forwarder';
+import { WETH9Contract } from '../../generated_contract_wrappers/weth9';
+import { artifacts } from '../utils/artifacts';
+import { expectTransactionFailedAsync } from '../utils/assertions';
+import { chaiSetup } from '../utils/chai_setup';
+import { constants } from '../utils/constants';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+import { ExchangeWrapper } from '../utils/exchange_wrapper';
+import { formatters } from '../utils/formatters';
+import { ForwarderWrapper } from '../utils/forwarder_wrapper';
+import { OrderFactory } from '../utils/order_factory';
+import { ContractName, ERC20BalancesByOwner } from '../utils/types';
+import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+const DECIMALS_DEFAULT = 18;
+// Set a gasPrice so when checking balance of msg.sender we can accurately calculate gasPrice*gasUsed
+const DEFAULT_GAS_PRICE = new BigNumber(1);
+
+describe(ContractName.Forwarder, () => {
+ let makerAddress: string;
+ let owner: string;
+ let takerAddress: string;
+ let feeRecipientAddress: string;
+ let otherAddress: string;
+ let defaultMakerAssetAddress: string;
+
+ let weth: DummyERC20TokenContract;
+ let zrxToken: DummyERC20TokenContract;
+ let erc721Token: DummyERC721TokenContract;
+ let forwarderContract: ForwarderContract;
+ let wethContract: WETH9Contract;
+ let forwarderWrapper: ForwarderWrapper;
+ let exchangeWrapper: ExchangeWrapper;
+
+ let signedOrder: SignedOrder;
+ let signedOrders: SignedOrder[];
+ let orderWithFee: SignedOrder;
+ let signedOrdersWithFee: SignedOrder[];
+ let feeOrder: SignedOrder;
+ let feeOrders: SignedOrder[];
+ let orderFactory: OrderFactory;
+ let erc20Wrapper: ERC20Wrapper;
+ let erc20Balances: ERC20BalancesByOwner;
+ let tx: TransactionReceiptWithDecodedLogs;
+
+ let erc721MakerAssetIds: BigNumber[];
+ let feeProportion: number = 0;
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress, otherAddress] = accounts);
+
+ const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+
+ const numDummyErc20ToDeploy = 3;
+ let erc20TokenA;
+ [erc20TokenA, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ const erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
+ const erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address];
+
+ wethContract = await WETH9Contract.deployFrom0xArtifactAsync(artifacts.EtherToken, provider, txDefaults);
+ weth = new DummyERC20TokenContract(wethContract.abi, wethContract.address, provider);
+ erc20Wrapper.addDummyTokenContract(weth);
+
+ const wethAssetData = assetProxyUtils.encodeERC20AssetData(wethContract.address);
+ const zrxAssetData = assetProxyUtils.encodeERC20AssetData(zrxToken.address);
+ const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ zrxAssetData,
+ );
+ const exchangeContract = new ExchangeContract(exchangeInstance.abi, exchangeInstance.address, provider);
+ exchangeWrapper = new ExchangeWrapper(exchangeContract, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
+ await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner);
+
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, {
+ from: owner,
+ });
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, {
+ from: owner,
+ });
+
+ defaultMakerAssetAddress = erc20TokenA.address;
+ const defaultTakerAssetAddress = wethContract.address;
+ const defaultOrderParams = {
+ exchangeAddress: exchangeInstance.address,
+ makerAddress,
+ feeRecipientAddress,
+ makerAssetData: assetProxyUtils.encodeERC20AssetData(defaultMakerAssetAddress),
+ takerAssetData: assetProxyUtils.encodeERC20AssetData(defaultTakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT),
+ makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT),
+ };
+ const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
+ orderFactory = new OrderFactory(privateKey, defaultOrderParams);
+
+ const forwarderInstance = await ForwarderContract.deployFrom0xArtifactAsync(
+ artifacts.Forwarder,
+ provider,
+ txDefaults,
+ exchangeInstance.address,
+ wethContract.address,
+ zrxToken.address,
+ AssetProxyId.ERC20,
+ zrxAssetData,
+ wethAssetData,
+ );
+ forwarderContract = new ForwarderContract(forwarderInstance.abi, forwarderInstance.address, provider);
+ forwarderWrapper = new ForwarderWrapper(forwarderContract, provider, zrxToken.address);
+ erc20Wrapper.addTokenOwnerAddress(forwarderInstance.address);
+
+ web3Wrapper.abiDecoder.addABI(forwarderContract.abi);
+ web3Wrapper.abiDecoder.addABI(exchangeInstance.abi);
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ feeProportion = 0;
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ signedOrder = orderFactory.newSignedOrder();
+ signedOrders = [signedOrder];
+ feeOrder = orderFactory.newSignedOrder({
+ makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT),
+ });
+ feeOrders = [feeOrder];
+ orderWithFee = orderFactory.newSignedOrder({
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT),
+ });
+ signedOrdersWithFee = [orderWithFee];
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('calculations', () => {
+ it('throws if partially filled orders passed in are not enough to satisfy requested amount', async () => {
+ feeOrders = [feeOrder];
+ const makerTokenFillAmount = feeOrder.makerAssetAmount.div(2);
+ const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ feeOrders,
+ [],
+ feeProportion,
+ makerTokenFillAmount,
+ );
+ // Fill the feeOrder
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync(feeOrders, [], makerTokenFillAmount, {
+ from: takerAddress,
+ value: fillAmountWei,
+ });
+ return expect(
+ forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ feeOrders,
+ [],
+ feeProportion,
+ makerTokenFillAmount,
+ ),
+ ).to.be.rejectedWith('Unable to satisfy makerAssetFillAmount with provided orders');
+ });
+ it('throws if orders passed are cancelled', async () => {
+ tx = await exchangeWrapper.cancelOrderAsync(feeOrder, makerAddress);
+ // Cancel the feeOrder
+ return expect(
+ forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ feeOrders,
+ [],
+ feeProportion,
+ feeOrder.makerAssetAmount.div(2),
+ ),
+ ).to.be.rejectedWith('Unable to satisfy makerAssetFillAmount with provided orders');
+ });
+ });
+ describe('marketSellEthForERC20 without extra fees', () => {
+ it('should fill the order', async () => {
+ const fillAmount = signedOrder.takerAssetAmount.div(2);
+ const makerBalanceBefore = erc20Balances[makerAddress][defaultMakerAssetAddress];
+ const takerBalanceBefore = erc20Balances[takerAddress][defaultMakerAssetAddress];
+ feeOrders = [];
+ tx = await forwarderWrapper.marketSellEthForERC20Async(signedOrders, feeOrders, {
+ value: fillAmount,
+ from: takerAddress,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerBalanceAfter = newBalances[makerAddress][defaultMakerAssetAddress];
+ const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress];
+ const makerTokenFillAmount = fillAmount
+ .times(signedOrder.makerAssetAmount)
+ .dividedToIntegerBy(signedOrder.takerAssetAmount);
+
+ expect(makerBalanceAfter).to.be.bignumber.equal(makerBalanceBefore.minus(makerTokenFillAmount));
+ expect(takerBalanceAfter).to.be.bignumber.equal(takerBalanceBefore.plus(makerTokenFillAmount));
+ expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0));
+ });
+ it('should fill the order and perform fee abstraction', async () => {
+ const fillAmount = signedOrder.takerAssetAmount.div(4);
+ const takerBalanceBefore = erc20Balances[takerAddress][defaultMakerAssetAddress];
+ tx = await forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, {
+ value: fillAmount,
+ from: takerAddress,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress];
+
+ const acceptPercentage = 98;
+ const acceptableThreshold = takerBalanceBefore.plus(fillAmount.times(acceptPercentage).dividedBy(100));
+ const isWithinThreshold = takerBalanceAfter.greaterThanOrEqualTo(acceptableThreshold);
+ expect(isWithinThreshold).to.be.true();
+ expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0));
+ });
+ it('should fill the order when token is ZRX with fees', async () => {
+ orderWithFee = orderFactory.newSignedOrder({
+ makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT),
+ });
+ signedOrdersWithFee = [orderWithFee];
+ feeOrders = [];
+ const fillAmount = signedOrder.takerAssetAmount.div(4);
+ const takerBalanceBefore = erc20Balances[takerAddress][zrxToken.address];
+ tx = await forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, {
+ value: fillAmount,
+ from: takerAddress,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const takerBalanceAfter = newBalances[takerAddress][zrxToken.address];
+
+ const acceptPercentage = 98;
+ const acceptableThreshold = takerBalanceBefore.plus(fillAmount.times(acceptPercentage).dividedBy(100));
+ const isWithinThreshold = takerBalanceAfter.greaterThanOrEqualTo(acceptableThreshold);
+ expect(isWithinThreshold).to.be.true();
+ expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0));
+ });
+ it('should fail if sent an ETH amount too high', async () => {
+ signedOrder = orderFactory.newSignedOrder({
+ makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT),
+ });
+ const fillAmount = signedOrder.takerAssetAmount.times(2);
+ return expectTransactionFailedAsync(
+ forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, {
+ value: fillAmount,
+ from: takerAddress,
+ }),
+ RevertReason.UnacceptableThreshold,
+ );
+ });
+ it('should fail if fee abstraction amount is too high', async () => {
+ orderWithFee = orderFactory.newSignedOrder({
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), DECIMALS_DEFAULT),
+ });
+ signedOrdersWithFee = [orderWithFee];
+ feeOrder = orderFactory.newSignedOrder({
+ makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT),
+ });
+ feeOrders = [feeOrder];
+ const fillAmount = signedOrder.takerAssetAmount.div(4);
+ return expectTransactionFailedAsync(
+ forwarderWrapper.marketSellEthForERC20Async(signedOrdersWithFee, feeOrders, {
+ value: fillAmount,
+ from: takerAddress,
+ }),
+ RevertReason.TransferFailed,
+ );
+ });
+ it('throws when mixed ERC721 and ERC20 assets with ERC20 first', async () => {
+ const makerAssetId = erc721MakerAssetIds[0];
+ const erc721SignedOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: new BigNumber(1),
+ makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ });
+ const erc20SignedOrder = orderFactory.newSignedOrder();
+ signedOrders = [erc20SignedOrder, erc721SignedOrder];
+ const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount);
+ return expectTransactionFailedAsync(
+ forwarderWrapper.marketSellEthForERC20Async(signedOrders, feeOrders, {
+ from: takerAddress,
+ value: fillAmountWei,
+ }),
+ RevertReason.InvalidOrderSignature,
+ );
+ });
+ });
+ describe('marketSellEthForERC20 with extra fees', () => {
+ it('should fill the order and send fee to fee recipient', async () => {
+ const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress);
+ const fillAmount = signedOrder.takerAssetAmount.div(2);
+ feeProportion = 150; // 1.5%
+ feeOrders = [];
+ tx = await forwarderWrapper.marketSellEthForERC20Async(
+ signedOrders,
+ feeOrders,
+ {
+ from: takerAddress,
+ value: fillAmount,
+ gasPrice: DEFAULT_GAS_PRICE,
+ },
+ {
+ feeProportion,
+ feeRecipient: feeRecipientAddress,
+ },
+ );
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const makerBalanceBefore = erc20Balances[makerAddress][defaultMakerAssetAddress];
+ const makerBalanceAfter = newBalances[makerAddress][defaultMakerAssetAddress];
+ const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress];
+ const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress);
+ const takerBoughtAmount = takerBalanceAfter.minus(erc20Balances[takerAddress][defaultMakerAssetAddress]);
+
+ expect(makerBalanceAfter).to.be.bignumber.equal(makerBalanceBefore.minus(takerBoughtAmount));
+ expect(afterEthBalance).to.be.bignumber.equal(
+ initEthBalance.plus(fillAmount.times(feeProportion).dividedBy(10000)),
+ );
+ expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(new BigNumber(0));
+ });
+ it('should fail if the fee is set too high', async () => {
+ const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress);
+ const fillAmount = signedOrder.takerAssetAmount.div(2);
+ feeProportion = 1500; // 15.0%
+ feeOrders = [];
+ await expectTransactionFailedAsync(
+ forwarderWrapper.marketSellEthForERC20Async(
+ signedOrders,
+ feeOrders,
+ { from: takerAddress, value: fillAmount, gasPrice: DEFAULT_GAS_PRICE },
+ { feeProportion, feeRecipient: feeRecipientAddress },
+ ),
+ RevertReason.FeeProportionTooLarge,
+ );
+ const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress);
+ expect(afterEthBalance).to.be.bignumber.equal(initEthBalance);
+ });
+ });
+ describe('marketBuyTokensWithEth', () => {
+ it('should buy the exact amount of assets', async () => {
+ const makerAssetAmount = signedOrder.makerAssetAmount.div(2);
+ const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const balancesBefore = await erc20Wrapper.getBalancesAsync();
+ const rate = signedOrder.makerAssetAmount.dividedBy(signedOrder.takerAssetAmount);
+ const fillAmountWei = makerAssetAmount.dividedToIntegerBy(rate);
+ feeOrders = [];
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: fillAmountWei,
+ gasPrice: DEFAULT_GAS_PRICE,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const takerBalanceBefore = balancesBefore[takerAddress][defaultMakerAssetAddress];
+ const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress];
+ const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const expectedEthBalanceAfterGasCosts = initEthBalance.minus(fillAmountWei).minus(tx.gasUsed);
+ expect(takerBalanceAfter).to.be.bignumber.eq(takerBalanceBefore.plus(makerAssetAmount));
+ expect(afterEthBalance).to.be.bignumber.eq(expectedEthBalanceAfterGasCosts);
+ });
+ it('should buy the exact amount of assets and return excess ETH', async () => {
+ const makerAssetAmount = signedOrder.makerAssetAmount.div(2);
+ const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const balancesBefore = await erc20Wrapper.getBalancesAsync();
+ const rate = signedOrder.makerAssetAmount.dividedBy(signedOrder.takerAssetAmount);
+ const fillAmount = makerAssetAmount.dividedToIntegerBy(rate);
+ const excessFillAmount = fillAmount.times(2);
+ feeOrders = [];
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: excessFillAmount,
+ gasPrice: DEFAULT_GAS_PRICE,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const takerBalanceBefore = balancesBefore[takerAddress][defaultMakerAssetAddress];
+ const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress];
+ const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const expectedEthBalanceAfterGasCosts = initEthBalance.minus(fillAmount).minus(tx.gasUsed);
+ expect(takerBalanceAfter).to.be.bignumber.eq(takerBalanceBefore.plus(makerAssetAmount));
+ expect(afterEthBalance).to.be.bignumber.eq(expectedEthBalanceAfterGasCosts);
+ });
+ it('should buy the exact amount of assets with fee abstraction', async () => {
+ const makerAssetAmount = signedOrder.makerAssetAmount.div(2);
+ const balancesBefore = await erc20Wrapper.getBalancesAsync();
+ const rate = signedOrder.makerAssetAmount.dividedBy(signedOrder.takerAssetAmount);
+ const fillAmount = makerAssetAmount.dividedToIntegerBy(rate);
+ const excessFillAmount = fillAmount.times(2);
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: excessFillAmount,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const takerBalanceBefore = balancesBefore[takerAddress][defaultMakerAssetAddress];
+ const takerBalanceAfter = newBalances[takerAddress][defaultMakerAssetAddress];
+ expect(takerBalanceAfter).to.be.bignumber.eq(takerBalanceBefore.plus(makerAssetAmount));
+ });
+ it('should buy the exact amount of assets when buying zrx with fee abstraction', async () => {
+ signedOrder = orderFactory.newSignedOrder({
+ makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT),
+ });
+ signedOrdersWithFee = [signedOrder];
+ feeOrders = [];
+ const makerAssetAmount = signedOrder.makerAssetAmount.div(2);
+ const takerWeiBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const balancesBefore = await erc20Wrapper.getBalancesAsync();
+ const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrdersWithFee,
+ feeOrders,
+ feeProportion,
+ makerAssetAmount,
+ );
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: fillAmountWei,
+ gasPrice: DEFAULT_GAS_PRICE,
+ });
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const takerTokenBalanceBefore = balancesBefore[takerAddress][zrxToken.address];
+ const takerTokenBalanceAfter = newBalances[takerAddress][zrxToken.address];
+ const takerWeiBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const expectedCostAfterGas = fillAmountWei.plus(tx.gasUsed);
+ expect(takerTokenBalanceAfter).to.be.bignumber.greaterThan(takerTokenBalanceBefore.plus(makerAssetAmount));
+ expect(takerWeiBalanceAfter).to.be.bignumber.equal(takerWeiBalanceBefore.minus(expectedCostAfterGas));
+ });
+ it('throws if fees are higher than 5% when buying zrx', async () => {
+ const highFeeZRXOrder = orderFactory.newSignedOrder({
+ makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address),
+ makerAssetAmount: signedOrder.makerAssetAmount,
+ takerFee: signedOrder.makerAssetAmount.times(0.06),
+ });
+ signedOrdersWithFee = [highFeeZRXOrder];
+ feeOrders = [];
+ const makerAssetAmount = signedOrder.makerAssetAmount.div(2);
+ const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrdersWithFee,
+ feeOrders,
+ feeProportion,
+ makerAssetAmount,
+ );
+ return expectTransactionFailedAsync(
+ forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: fillAmountWei,
+ }),
+ RevertReason.UnacceptableThreshold,
+ );
+ });
+ it('throws if fees are higher than 5% when buying erc20', async () => {
+ const highFeeERC20Order = orderFactory.newSignedOrder({
+ takerFee: signedOrder.makerAssetAmount.times(0.06),
+ });
+ signedOrdersWithFee = [highFeeERC20Order];
+ feeOrders = [feeOrder];
+ const makerAssetAmount = signedOrder.makerAssetAmount.div(2);
+ const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrdersWithFee,
+ feeOrders,
+ feeProportion,
+ makerAssetAmount,
+ );
+ return expectTransactionFailedAsync(
+ forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: fillAmountWei,
+ }),
+ RevertReason.UnacceptableThreshold as any,
+ );
+ });
+ it('throws if makerAssetAmount is 0', async () => {
+ const makerAssetAmount = new BigNumber(0);
+ const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrdersWithFee,
+ feeOrders,
+ feeProportion,
+ makerAssetAmount,
+ );
+ return expectTransactionFailedAsync(
+ forwarderWrapper.marketBuyTokensWithEthAsync(signedOrdersWithFee, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: fillAmountWei,
+ }),
+ RevertReason.ValueGreaterThanZero as any,
+ );
+ });
+ it('throws if the amount of ETH sent in is less than the takerAssetFilledAmount', async () => {
+ const makerAssetAmount = signedOrder.makerAssetAmount;
+ const fillAmount = signedOrder.takerAssetAmount.div(2);
+ const zero = new BigNumber(0);
+ // Deposit enough taker balance to fill the order
+ const wethDepositTxHash = await wethContract.deposit.sendTransactionAsync({
+ from: takerAddress,
+ value: signedOrder.takerAssetAmount,
+ });
+ await web3Wrapper.awaitTransactionSuccessAsync(wethDepositTxHash);
+ // Transfer all of this WETH to the forwarding contract
+ const wethTransferTxHash = await wethContract.transfer.sendTransactionAsync(
+ forwarderContract.address,
+ signedOrder.takerAssetAmount,
+ { from: takerAddress },
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(wethTransferTxHash);
+ // We use the contract directly to get around wrapper validations and calculations
+ const formattedOrders = formatters.createMarketSellOrders(signedOrders, zero);
+ const formattedFeeOrders = formatters.createMarketSellOrders(feeOrders, zero);
+ return expectTransactionFailedAsync(
+ forwarderContract.marketBuyTokensWithEth.sendTransactionAsync(
+ formattedOrders.orders,
+ formattedOrders.signatures,
+ formattedFeeOrders.orders,
+ formattedFeeOrders.signatures,
+ makerAssetAmount,
+ zero,
+ constants.NULL_ADDRESS,
+ { value: fillAmount, from: takerAddress },
+ ),
+ RevertReason.InvalidMsgValue,
+ );
+ });
+ });
+ describe('marketBuyTokensWithEth - ERC721', async () => {
+ it('buys ERC721 assets', async () => {
+ const makerAssetId = erc721MakerAssetIds[0];
+ signedOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: new BigNumber(1),
+ makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ });
+ feeOrders = [];
+ signedOrders = [signedOrder];
+ const makerAssetAmount = new BigNumber(signedOrders.length);
+ const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrders,
+ feeOrders,
+ feeProportion,
+ makerAssetAmount,
+ );
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: fillAmountWei,
+ });
+ const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ });
+ it('buys ERC721 assets with fee abstraction', async () => {
+ const makerAssetId = erc721MakerAssetIds[0];
+ signedOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: new BigNumber(1),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT),
+ makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ });
+ signedOrders = [signedOrder];
+ const makerAssetAmount = new BigNumber(signedOrders.length);
+ const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrders,
+ feeOrders,
+ feeProportion,
+ makerAssetAmount,
+ );
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: fillAmountWei,
+ });
+ const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ });
+ it('buys ERC721 assets with fee abstraction and pays fee to fee recipient', async () => {
+ const makerAssetId = erc721MakerAssetIds[0];
+ signedOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: new BigNumber(1),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT),
+ makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ });
+ signedOrders = [signedOrder];
+ feeProportion = 100;
+ const initTakerBalanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const initFeeRecipientBalanceWei = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress);
+ const makerAssetAmount = new BigNumber(signedOrders.length);
+ const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrders,
+ feeOrders,
+ feeProportion,
+ makerAssetAmount,
+ );
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync(
+ signedOrders,
+ feeOrders,
+ makerAssetAmount,
+ {
+ from: takerAddress,
+ value: fillAmountWei,
+ gasPrice: DEFAULT_GAS_PRICE,
+ },
+ {
+ feeProportion,
+ feeRecipient: feeRecipientAddress,
+ },
+ );
+ const afterFeeRecipientEthBalance = await web3Wrapper.getBalanceInWeiAsync(feeRecipientAddress);
+ const afterTakerBalanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const takerFilledAmount = initTakerBalanceWei.minus(afterTakerBalanceWei).plus(tx.gasUsed);
+ const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ const balanceDiff = afterFeeRecipientEthBalance.minus(initFeeRecipientBalanceWei);
+ expect(takerFilledAmount.dividedToIntegerBy(balanceDiff)).to.be.bignumber.equal(101);
+ expect(takerFilledAmount.minus(balanceDiff).dividedToIntegerBy(balanceDiff)).to.be.bignumber.equal(100);
+ });
+ it('buys multiple ERC721 assets with fee abstraction and pays fee to fee recipient', async () => {
+ const makerAssetId1 = erc721MakerAssetIds[0];
+ const makerAssetId2 = erc721MakerAssetIds[1];
+ const signedOrder1 = orderFactory.newSignedOrder({
+ makerAssetAmount: new BigNumber(1),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(3), DECIMALS_DEFAULT),
+ makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId1),
+ });
+ const signedOrder2 = orderFactory.newSignedOrder({
+ makerAssetAmount: new BigNumber(1),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(4), DECIMALS_DEFAULT),
+ makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId2),
+ });
+ signedOrders = [signedOrder1, signedOrder2];
+ feeProportion = 10;
+ const makerAssetAmount = new BigNumber(signedOrders.length);
+ const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrders,
+ feeOrders,
+ feeProportion,
+ makerAssetAmount,
+ );
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: fillAmountWei,
+ });
+ const newOwnerTakerAsset1 = await erc721Token.ownerOf.callAsync(makerAssetId1);
+ expect(newOwnerTakerAsset1).to.be.bignumber.equal(takerAddress);
+ const newOwnerTakerAsset2 = await erc721Token.ownerOf.callAsync(makerAssetId2);
+ expect(newOwnerTakerAsset2).to.be.bignumber.equal(takerAddress);
+ });
+ it('buys ERC721 assets with fee abstraction and handles fee orders filled and excess eth', async () => {
+ const makerAssetId = erc721MakerAssetIds[0];
+ feeProportion = 0;
+ // In this scenario a total of 6 ZRX fees need to be paid.
+ // There are two fee orders, but the first fee order is partially filled while
+ // the Forwarding contract tx is in the mempool.
+ const erc721MakerAssetAmount = new BigNumber(1);
+ signedOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: erc721MakerAssetAmount,
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT),
+ makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ });
+ signedOrders = [signedOrder];
+ const firstFeeOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT),
+ makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT),
+ });
+ const secondFeeOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT),
+ makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT),
+ });
+ feeOrders = [firstFeeOrder, secondFeeOrder];
+ const makerAssetAmount = new BigNumber(signedOrders.length);
+ const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrders,
+ feeOrders,
+ feeProportion,
+ erc721MakerAssetAmount,
+ );
+ // Simulate another otherAddress user partially filling firstFeeOrder
+ const firstFeeOrderFillAmount = firstFeeOrder.makerAssetAmount.div(2);
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync([firstFeeOrder], [], firstFeeOrderFillAmount, {
+ from: otherAddress,
+ value: fillAmountWei,
+ });
+ // For tests we calculate how much this should've cost given that firstFeeOrder was filled
+ const expectedFillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrders,
+ feeOrders,
+ feeProportion,
+ erc721MakerAssetAmount,
+ );
+ // With 4 ZRX remaining in firstFeeOrder, the secondFeeOrder will need to be filled to make up
+ // the total amount of fees required (6)
+ // Since the fee orders can be filled while the transaction is pending the user safely sends in
+ // extra ether to cover any slippage
+ const initEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const slippageFillAmountWei = fillAmountWei.times(2);
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: slippageFillAmountWei,
+ gasPrice: DEFAULT_GAS_PRICE,
+ });
+ const afterEthBalance = await web3Wrapper.getBalanceInWeiAsync(takerAddress);
+ const expectedEthBalanceAfterGasCosts = initEthBalance.minus(expectedFillAmountWei).minus(tx.gasUsed);
+ const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ expect(afterEthBalance).to.be.bignumber.equal(expectedEthBalanceAfterGasCosts);
+ });
+ it('buys ERC721 assets with fee abstraction and handles fee orders filled', async () => {
+ const makerAssetId = erc721MakerAssetIds[0];
+ feeProportion = 0;
+ // In this scenario a total of 6 ZRX fees need to be paid.
+ // There are two fee orders, but the first fee order is partially filled while
+ // the Forwarding contract tx is in the mempool.
+ const erc721MakerAssetAmount = new BigNumber(1);
+ signedOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: erc721MakerAssetAmount,
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(6), DECIMALS_DEFAULT),
+ makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ });
+ const zrxMakerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(8), DECIMALS_DEFAULT);
+ signedOrders = [signedOrder];
+ const firstFeeOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: zrxMakerAssetAmount,
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.1), DECIMALS_DEFAULT),
+ makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT),
+ });
+ const secondFeeOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: zrxMakerAssetAmount,
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(0.12), DECIMALS_DEFAULT),
+ makerAssetData: assetProxyUtils.encodeERC20AssetData(zrxToken.address),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT),
+ });
+ feeOrders = [firstFeeOrder, secondFeeOrder];
+ const makerAssetAmount = new BigNumber(signedOrders.length);
+ const fillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrders,
+ feeOrders,
+ feeProportion,
+ erc721MakerAssetAmount,
+ );
+ // Simulate another otherAddress user partially filling firstFeeOrder
+ const firstFeeOrderFillAmount = firstFeeOrder.makerAssetAmount.div(2);
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync([firstFeeOrder], [], firstFeeOrderFillAmount, {
+ from: otherAddress,
+ value: fillAmountWei,
+ });
+ const expectedFillAmountWei = await forwarderWrapper.calculateMarketBuyFillAmountWeiAsync(
+ signedOrders,
+ feeOrders,
+ feeProportion,
+ erc721MakerAssetAmount,
+ );
+ tx = await forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: expectedFillAmountWei,
+ });
+ const newOwnerTakerAsset = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(newOwnerTakerAsset).to.be.bignumber.equal(takerAddress);
+ });
+ it('throws when mixed ERC721 and ERC20 assets', async () => {
+ const makerAssetId = erc721MakerAssetIds[0];
+ const erc721SignedOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: new BigNumber(1),
+ makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ });
+ const erc20SignedOrder = orderFactory.newSignedOrder();
+ signedOrders = [erc721SignedOrder, erc20SignedOrder];
+ const makerAssetAmount = new BigNumber(signedOrders.length);
+ const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount);
+ return expectTransactionFailedAsync(
+ forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: fillAmountWei,
+ }),
+ RevertReason.LibBytesGreaterOrEqualTo32LengthRequired,
+ );
+ });
+ it('throws when mixed ERC721 and ERC20 assets with ERC20 first', async () => {
+ const makerAssetId = erc721MakerAssetIds[0];
+ const erc721SignedOrder = orderFactory.newSignedOrder({
+ makerAssetAmount: new BigNumber(1),
+ makerAssetData: assetProxyUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ });
+ const erc20SignedOrder = orderFactory.newSignedOrder();
+ signedOrders = [erc20SignedOrder, erc721SignedOrder];
+ const makerAssetAmount = new BigNumber(signedOrders.length);
+ const fillAmountWei = erc20SignedOrder.takerAssetAmount.plus(erc721SignedOrder.takerAssetAmount);
+ return expectTransactionFailedAsync(
+ forwarderWrapper.marketBuyTokensWithEthAsync(signedOrders, feeOrders, makerAssetAmount, {
+ from: takerAddress,
+ value: fillAmountWei,
+ }),
+ RevertReason.InvalidTakerAmount,
+ );
+ });
+ });
+});
+// tslint:disable:max-file-line-count
+// tslint:enable:no-unnecessary-type-assertion
diff --git a/packages/contracts/test/utils/artifacts.ts b/packages/contracts/test/utils/artifacts.ts
index 23e93c085..d3f808218 100644
--- a/packages/contracts/test/utils/artifacts.ts
+++ b/packages/contracts/test/utils/artifacts.ts
@@ -8,6 +8,7 @@ import * as ERC20Proxy from '../../artifacts/ERC20Proxy.json';
import * as ERC721Proxy from '../../artifacts/ERC721Proxy.json';
import * as Exchange from '../../artifacts/Exchange.json';
import * as ExchangeWrapper from '../../artifacts/ExchangeWrapper.json';
+import * as Forwarder from '../../artifacts/Forwarder.json';
import * as IAssetProxy from '../../artifacts/IAssetProxy.json';
import * as MixinAuthorizable from '../../artifacts/MixinAuthorizable.json';
import * as MultiSigWallet from '../../artifacts/MultiSigWallet.json';
@@ -34,6 +35,7 @@ export const artifacts = {
Exchange: (Exchange as any) as ContractArtifact,
ExchangeWrapper: (ExchangeWrapper as any) as ContractArtifact,
EtherToken: (EtherToken as any) as ContractArtifact,
+ Forwarder: (Forwarder as any) as ContractArtifact,
IAssetProxy: (IAssetProxy as any) as ContractArtifact,
MixinAuthorizable: (MixinAuthorizable as any) as ContractArtifact,
MultiSigWallet: (MultiSigWallet as any) as ContractArtifact,
diff --git a/packages/contracts/test/utils/erc20_wrapper.ts b/packages/contracts/test/utils/erc20_wrapper.ts
index 0b42f1909..cf1433791 100644
--- a/packages/contracts/test/utils/erc20_wrapper.ts
+++ b/packages/contracts/test/utils/erc20_wrapper.ts
@@ -138,6 +138,14 @@ export class ERC20Wrapper {
});
return balancesByOwner;
}
+ public addDummyTokenContract(dummy: DummyERC20TokenContract): void {
+ if (!_.isUndefined(this._dummyTokenContracts)) {
+ this._dummyTokenContracts.push(dummy);
+ }
+ }
+ public addTokenOwnerAddress(address: string): void {
+ this._tokenOwnerAddresses.push(address);
+ }
public getTokenOwnerAddresses(): string[] {
return this._tokenOwnerAddresses;
}
diff --git a/packages/contracts/test/utils/forwarder_wrapper.ts b/packages/contracts/test/utils/forwarder_wrapper.ts
new file mode 100644
index 000000000..d227420ee
--- /dev/null
+++ b/packages/contracts/test/utils/forwarder_wrapper.ts
@@ -0,0 +1,220 @@
+import { assetProxyUtils } from '@0xproject/order-utils';
+import { AssetProxyId, SignedOrder } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import { Provider, TransactionReceiptWithDecodedLogs, TxDataPayable } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { ForwarderContract } from '../../generated_contract_wrappers/forwarder';
+
+import { constants } from './constants';
+import { formatters } from './formatters';
+import { LogDecoder } from './log_decoder';
+import { MarketSellOrders } from './types';
+
+const DEFAULT_FEE_PROPORTION = 0;
+const PERCENTAGE_DENOMINATOR = 10000;
+const ZERO_AMOUNT = new BigNumber(0);
+const INSUFFICENT_ORDERS_FOR_MAKER_AMOUNT = 'Unable to satisfy makerAssetFillAmount with provided orders';
+
+export class ForwarderWrapper {
+ private _web3Wrapper: Web3Wrapper;
+ private _forwarderContract: ForwarderContract;
+ private _logDecoder: LogDecoder;
+ private _zrxAddress: string;
+ private static _createOptimizedSellOrders(signedOrders: SignedOrder[]): MarketSellOrders {
+ const marketSellOrders = formatters.createMarketSellOrders(signedOrders, ZERO_AMOUNT);
+ const assetDataId = assetProxyUtils.decodeAssetDataId(signedOrders[0].makerAssetData);
+ // Contract will fill this in for us as all of the assetData is assumed to be the same
+ for (let i = 0; i < signedOrders.length; i++) {
+ if (i !== 0 && assetDataId === AssetProxyId.ERC20) {
+ // Forwarding contract will fill this in from the first order
+ marketSellOrders.orders[i].makerAssetData = constants.NULL_BYTES;
+ }
+ marketSellOrders.orders[i].takerAssetData = constants.NULL_BYTES;
+ }
+ return marketSellOrders;
+ }
+ private static _createOptimizedZRXSellOrders(signedOrders: SignedOrder[]): MarketSellOrders {
+ const marketSellOrders = formatters.createMarketSellOrders(signedOrders, ZERO_AMOUNT);
+ // Contract will fill this in for us as all of the assetData is assumed to be the same
+ for (let i = 0; i < signedOrders.length; i++) {
+ marketSellOrders.orders[i].makerAssetData = constants.NULL_BYTES;
+ marketSellOrders.orders[i].takerAssetData = constants.NULL_BYTES;
+ }
+ return marketSellOrders;
+ }
+ private static _calculateAdditionalFeeProportionAmount(feeProportion: number, fillAmountWei: BigNumber): BigNumber {
+ if (feeProportion > 0) {
+ // Add to the total ETH transaction to ensure all NFTs can be filled after fees
+ // 150 = 1.5% = 0.015
+ const denominator = new BigNumber(1).minus(new BigNumber(feeProportion).dividedBy(PERCENTAGE_DENOMINATOR));
+ return fillAmountWei.dividedBy(denominator).round(0, BigNumber.ROUND_FLOOR);
+ }
+ return fillAmountWei;
+ }
+ constructor(contractInstance: ForwarderContract, provider: Provider, zrxAddress: string) {
+ this._forwarderContract = contractInstance;
+ this._web3Wrapper = new Web3Wrapper(provider);
+ this._logDecoder = new LogDecoder(this._web3Wrapper, this._forwarderContract.address);
+ // this._web3Wrapper.abiDecoder.addABI(contractInstance.abi);
+ this._zrxAddress = zrxAddress;
+ }
+ public async marketBuyTokensWithEthAsync(
+ orders: SignedOrder[],
+ feeOrders: SignedOrder[],
+ makerTokenBuyAmount: BigNumber,
+ txData: TxDataPayable,
+ opts: { feeProportion?: number; feeRecipient?: string } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const params = ForwarderWrapper._createOptimizedSellOrders(orders);
+ const feeParams = ForwarderWrapper._createOptimizedZRXSellOrders(feeOrders);
+ const feeProportion = _.isUndefined(opts.feeProportion) ? DEFAULT_FEE_PROPORTION : opts.feeProportion;
+ const feeRecipient = _.isUndefined(opts.feeRecipient) ? constants.NULL_ADDRESS : opts.feeRecipient;
+ const txHash: string = await this._forwarderContract.marketBuyTokensWithEth.sendTransactionAsync(
+ params.orders,
+ params.signatures,
+ feeParams.orders,
+ feeParams.signatures,
+ makerTokenBuyAmount,
+ feeProportion,
+ feeRecipient,
+ txData,
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async marketSellEthForERC20Async(
+ orders: SignedOrder[],
+ feeOrders: SignedOrder[],
+ txData: TxDataPayable,
+ opts: { feeProportion?: number; feeRecipient?: string } = {},
+ ): Promise<TransactionReceiptWithDecodedLogs> {
+ const assetDataId = assetProxyUtils.decodeAssetDataId(orders[0].makerAssetData);
+ if (assetDataId !== AssetProxyId.ERC20) {
+ throw new Error('Asset type not supported by marketSellEthForERC20');
+ }
+ const params = ForwarderWrapper._createOptimizedSellOrders(orders);
+ const feeParams = ForwarderWrapper._createOptimizedZRXSellOrders(feeOrders);
+ const feeProportion = _.isUndefined(opts.feeProportion) ? DEFAULT_FEE_PROPORTION : opts.feeProportion;
+ const feeRecipient = _.isUndefined(opts.feeRecipient) ? constants.NULL_ADDRESS : opts.feeRecipient;
+ const txHash: string = await this._forwarderContract.marketSellEthForERC20.sendTransactionAsync(
+ params.orders,
+ params.signatures,
+ feeParams.orders,
+ feeParams.signatures,
+ feeProportion,
+ feeRecipient,
+ txData,
+ );
+ const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
+ return tx;
+ }
+ public async calculateMarketBuyFillAmountWeiAsync(
+ orders: SignedOrder[],
+ feeOrders: SignedOrder[],
+ feeProportion: number,
+ makerAssetFillAmount: BigNumber,
+ ): Promise<BigNumber> {
+ const assetProxyId = assetProxyUtils.decodeAssetDataId(orders[0].makerAssetData);
+ switch (assetProxyId) {
+ case AssetProxyId.ERC20: {
+ const fillAmountWei = this._calculateMarketBuyERC20FillAmountAsync(
+ orders,
+ feeOrders,
+ feeProportion,
+ makerAssetFillAmount,
+ );
+ return fillAmountWei;
+ }
+ case AssetProxyId.ERC721: {
+ const fillAmountWei = await this._calculateMarketBuyERC721FillAmountAsync(
+ orders,
+ feeOrders,
+ feeProportion,
+ );
+ return fillAmountWei;
+ }
+ default:
+ throw new Error(`Invalid Asset Proxy Id: ${assetProxyId}`);
+ }
+ }
+ private async _calculateMarketBuyERC20FillAmountAsync(
+ orders: SignedOrder[],
+ feeOrders: SignedOrder[],
+ feeProportion: number,
+ makerAssetFillAmount: BigNumber,
+ ): Promise<BigNumber> {
+ const makerAssetData = assetProxyUtils.decodeAssetData(orders[0].makerAssetData);
+ const makerAssetToken = makerAssetData.tokenAddress;
+ const params = formatters.createMarketBuyOrders(orders, makerAssetFillAmount);
+
+ let fillAmountWei;
+ if (makerAssetToken === this._zrxAddress) {
+ // If buying ZRX we buy the tokens and fees from the ZRX order in one step
+ const expectedBuyFeeTokensFillResults = await this._forwarderContract.calculateMarketBuyZrxResults.callAsync(
+ params.orders,
+ makerAssetFillAmount,
+ );
+ if (expectedBuyFeeTokensFillResults.makerAssetFilledAmount.lessThan(makerAssetFillAmount)) {
+ throw new Error(INSUFFICENT_ORDERS_FOR_MAKER_AMOUNT);
+ }
+ fillAmountWei = expectedBuyFeeTokensFillResults.takerAssetFilledAmount;
+ } else {
+ const expectedMarketBuyFillResults = await this._forwarderContract.calculateMarketBuyResults.callAsync(
+ params.orders,
+ makerAssetFillAmount,
+ );
+ if (expectedMarketBuyFillResults.makerAssetFilledAmount.lessThan(makerAssetFillAmount)) {
+ throw new Error(INSUFFICENT_ORDERS_FOR_MAKER_AMOUNT);
+ }
+ fillAmountWei = expectedMarketBuyFillResults.takerAssetFilledAmount;
+ const expectedFeeAmount = expectedMarketBuyFillResults.takerFeePaid;
+ if (expectedFeeAmount.greaterThan(ZERO_AMOUNT)) {
+ const expectedFeeFillFillAmountWei = await this._calculateMarketBuyERC20FillAmountAsync(
+ feeOrders,
+ [],
+ DEFAULT_FEE_PROPORTION,
+ expectedFeeAmount,
+ );
+ fillAmountWei = fillAmountWei.plus(expectedFeeFillFillAmountWei);
+ }
+ }
+ fillAmountWei = ForwarderWrapper._calculateAdditionalFeeProportionAmount(feeProportion, fillAmountWei);
+ return fillAmountWei;
+ }
+ private async _calculateMarketBuyERC721FillAmountAsync(
+ orders: SignedOrder[],
+ feeOrders: SignedOrder[],
+ feeProportion: number,
+ ): Promise<BigNumber> {
+ // Total cost when buying ERC721 is the total cost of all ERC721 orders + any fee abstraction
+ let fillAmountWei = _.reduce(
+ orders,
+ (totalAmount: BigNumber, order: SignedOrder) => {
+ return totalAmount.plus(order.takerAssetAmount);
+ },
+ ZERO_AMOUNT,
+ );
+ const totalFees = _.reduce(
+ orders,
+ (totalAmount: BigNumber, order: SignedOrder) => {
+ return totalAmount.plus(order.takerFee);
+ },
+ ZERO_AMOUNT,
+ );
+ if (totalFees.greaterThan(ZERO_AMOUNT)) {
+ // Calculate the ZRX fee abstraction cost
+ const emptyFeeOrders: SignedOrder[] = [];
+ const expectedFeeAmountWei = await this._calculateMarketBuyERC20FillAmountAsync(
+ feeOrders,
+ emptyFeeOrders,
+ DEFAULT_FEE_PROPORTION,
+ totalFees,
+ );
+ fillAmountWei = fillAmountWei.plus(expectedFeeAmountWei);
+ }
+ fillAmountWei = ForwarderWrapper._calculateAdditionalFeeProportionAmount(feeProportion, fillAmountWei);
+ return fillAmountWei;
+ }
+}
diff --git a/packages/contracts/test/utils/types.ts b/packages/contracts/test/utils/types.ts
index b792bb90a..67313b647 100644
--- a/packages/contracts/test/utils/types.ts
+++ b/packages/contracts/test/utils/types.ts
@@ -102,6 +102,7 @@ export enum ContractName {
TestWallet = 'TestWallet',
Authorizable = 'Authorizable',
Whitelist = 'Whitelist',
+ Forwarder = 'Forwarder',
}
export interface SignedTransaction {
@@ -227,3 +228,10 @@ export interface FillScenario {
makerStateScenario: TraderStateScenario;
takerStateScenario: TraderStateScenario;
}
+
+export interface FillResults {
+ makerAssetFilledAmount: BigNumber;
+ takerAssetFilledAmount: BigNumber;
+ makerFeePaid: BigNumber;
+ takerFeePaid: BigNumber;
+}