import { ContractAddresses, ContractWrappers } from '@0x/contract-wrappers'; import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils'; import { assetDataUtils } from '@0x/order-utils'; import { MarketOperation, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as chai from 'chai'; import 'mocha'; import { SwapQuote } from '../src'; import { constants } from '../src/constants'; import { ForwarderSwapQuoteConsumer } from '../src/quote_consumers/forwarder_swap_quote_consumer'; import { ForwarderMarketBuySmartContractParams, ForwarderMarketSellSmartContractParams, MarketBuySwapQuote, } from '../src/types'; import { chaiSetup } from './utils/chai_setup'; import { migrateOnceAsync } from './utils/migrate'; import { getFullyFillableSwapQuoteWithNoFees, getSignedOrdersWithNoFeesAsync } from './utils/swap_quote'; import { provider, web3Wrapper } from './utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); const TESTRPC_NETWORK_ID = 50; const MARKET_OPERATION = MarketOperation.Sell; const FILLABLE_AMOUNTS = [new BigNumber(2), new BigNumber(3), new BigNumber(5)].map(value => value.multipliedBy(ONE_ETH_IN_WEI), ); describe('ForwarderSwapQuoteConsumer', () => { let contractWrappers: ContractWrappers; let userAddresses: string[]; let coinbaseAddress: string; let makerAddress: string; let takerAddress: string; let feeRecipient: string; let makerTokenAddress: string; let takerTokenAddress: string; let makerAssetData: string; let takerAssetData: string; let wethAssetData: string; let contractAddresses: ContractAddresses; let orders: SignedOrder[]; let marketSellSwapQuote: SwapQuote; let marketBuySwapQuote: SwapQuote; let swapQuoteConsumer: ForwarderSwapQuoteConsumer; let erc20ProxyAddress: string; const networkId = TESTRPC_NETWORK_ID; before(async () => { contractAddresses = await migrateOnceAsync(); await blockchainLifecycle.startAsync(); userAddresses = await web3Wrapper.getAvailableAddressesAsync(); const config = { networkId, contractAddresses, }; contractWrappers = new ContractWrappers(provider, config); [coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses; [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); [makerAssetData, takerAssetData, wethAssetData] = [ assetDataUtils.encodeERC20AssetData(makerTokenAddress), assetDataUtils.encodeERC20AssetData(takerTokenAddress), assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken), ]; }); after(async () => { await blockchainLifecycle.revertAsync(); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); const UNLIMITED_ALLOWANCE = contractWrappers.erc20Token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; erc20ProxyAddress = contractWrappers.erc20Proxy.address; const totalFillableAmount = FILLABLE_AMOUNTS.reduce( (a: BigNumber, c: BigNumber) => a.plus(c), new BigNumber(0), ); await contractWrappers.erc20Token.transferAsync( makerTokenAddress, coinbaseAddress, makerAddress, totalFillableAmount, ); await contractWrappers.erc20Token.setAllowanceAsync( makerTokenAddress, makerAddress, erc20ProxyAddress, UNLIMITED_ALLOWANCE, ); orders = await getSignedOrdersWithNoFeesAsync( provider, makerAssetData, wethAssetData, makerAddress, takerAddress, FILLABLE_AMOUNTS, contractAddresses.exchange, ); marketSellSwapQuote = getFullyFillableSwapQuoteWithNoFees( makerAssetData, wethAssetData, orders, MarketOperation.Sell, ); marketBuySwapQuote = getFullyFillableSwapQuoteWithNoFees( makerAssetData, wethAssetData, orders, MarketOperation.Buy, ); swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, { networkId, }); }); afterEach(async () => { await blockchainLifecycle.revertAsync(); }); describe('executeSwapQuoteOrThrowAsync', () => { describe('validation', () => { it('should throw if swapQuote provided is not a valid forwarder SwapQuote (taker asset is wEth', async () => { const invalidSignedOrders = await getSignedOrdersWithNoFeesAsync( provider, makerAssetData, takerAssetData, makerAddress, takerAddress, FILLABLE_AMOUNTS, ); const invalidSwapQuote = getFullyFillableSwapQuoteWithNoFees( makerAssetData, takerAssetData, invalidSignedOrders, MARKET_OPERATION, ); expect( swapQuoteConsumer.executeSwapQuoteOrThrowAsync(invalidSwapQuote, { takerAddress }), ).to.be.rejectedWith( `Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`, ); }); }); // TODO(david) test execution of swap quotes with fee orders describe('valid swap quote', () => { /* * Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert) * Does not test the validity of the state change performed by the forwarder smart contract */ it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => { let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { takerAddress }); makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); expect(makerBalance).to.bignumber.equal(new BigNumber(0.5).multipliedBy(ONE_ETH_IN_WEI)); expect(takerBalance).to.bignumber.equal(new BigNumber(9.5).multipliedBy(ONE_ETH_IN_WEI)); }); it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => { let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { takerAddress }); makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); }); it('should perform a marketBuy execution with affiliate fees', async () => { let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { takerAddress, feePercentage: 0.05, feeRecipient, }); makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal( new BigNumber(0.5).multipliedBy(ONE_ETH_IN_WEI), ); }); // TODO(david) Finish marketSell affiliate fee excution testing // it('should perform a marketSell execution with affiliate fees', async () => { // let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); // let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); // const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); // expect(makerBalance).to.bignumber.equal((new BigNumber(10)).multipliedBy(ONE_ETH_IN_WEI)); // expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); // console.log(makerBalance, takerBalance, feeRecipientEthBalanceBefore); // await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { takerAddress, feePercentage: 0.05, feeRecipient }); // makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); // takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); // const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); // console.log(makerBalance, takerBalance, feeRecipientEthBalanceAfter); // expect(makerBalance).to.bignumber.equal((new BigNumber(0.5)).multipliedBy(ONE_ETH_IN_WEI)); // expect(takerBalance).to.bignumber.equal((new BigNumber(9.5)).multipliedBy(ONE_ETH_IN_WEI)); // expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal((new BigNumber(0.5)).multipliedBy(ONE_ETH_IN_WEI)); // }); }); }); describe('getSmartContractParamsOrThrow', () => { describe('validation', () => { it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => { const invalidSignedOrders = await getSignedOrdersWithNoFeesAsync( provider, makerAssetData, takerAssetData, makerAddress, takerAddress, FILLABLE_AMOUNTS, ); const invalidSwapQuote = getFullyFillableSwapQuoteWithNoFees( makerAssetData, takerAssetData, invalidSignedOrders, MARKET_OPERATION, ); expect(swapQuoteConsumer.getSmartContractParamsOrThrowAsync(invalidSwapQuote, {})).to.be.rejectedWith( `Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`, ); }); }); describe('valid swap quote', async () => { it('provide correct and optimized smart contract params with default options for a marketSell SwapQuote (no affiliate fees)', async () => { const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync( marketSellSwapQuote, {}, ); expect(toAddress).to.deep.equal(contractWrappers.forwarder.address); const { feeSignatures, feePercentage, feeRecipient: feeRecipientFromParams, signatures, type, } = params as ForwarderMarketSellSmartContractParams; expect(type).to.deep.equal(MarketOperation.Sell); expect(feeRecipientFromParams).to.deep.equal(constants.NULL_ADDRESS); const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature); expect(signatures).to.deep.equal(orderSignatures); expect(feePercentage).to.bignumber.equal(0); expect(feeSignatures).to.deep.equal([]); }); it('provide correct and optimized smart contract params with default options for a marketBuy SwapQuote (no affiliate fees)', async () => { const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync( marketBuySwapQuote, {}, ); expect(toAddress).to.deep.equal(contractWrappers.forwarder.address); const { makerAssetFillAmount, feeSignatures, feePercentage, feeRecipient: feeRecipientFromParams, signatures, type, } = params as ForwarderMarketBuySmartContractParams; expect(type).to.deep.equal(MarketOperation.Buy); expect(feeRecipientFromParams).to.deep.equal(constants.NULL_ADDRESS); expect(makerAssetFillAmount).to.bignumber.equal( (marketBuySwapQuote as MarketBuySwapQuote).makerAssetFillAmount, ); const orderSignatures = marketBuySwapQuote.orders.map(order => order.signature); expect(signatures).to.deep.equal(orderSignatures); expect(feePercentage).to.bignumber.equal(0); expect(feeSignatures).to.deep.equal([]); }); it('provide correct and optimized smart contract params with affiliate fees for a marketSell SwapQuote', async () => { const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync( marketSellSwapQuote, { feePercentage: 0.05, feeRecipient, }, ); expect(toAddress).to.deep.equal(contractWrappers.forwarder.address); const { feeSignatures, feePercentage, feeRecipient: feeRecipientFromParams, signatures, type, } = params as ForwarderMarketSellSmartContractParams; expect(type).to.deep.equal(MarketOperation.Sell); expect(feeRecipientFromParams).to.deep.equal(feeRecipient); const orderSignatures = marketSellSwapQuote.orders.map(order => order.signature); expect(signatures).to.deep.equal(orderSignatures); expect(feePercentage).to.bignumber.equal(new BigNumber(0.05).multipliedBy(ONE_ETH_IN_WEI)); expect(feeSignatures).to.deep.equal([]); }); it('provide correct and optimized smart contract params with affiliate fees for a marketBuy SwapQuote', async () => { const { toAddress, params } = await swapQuoteConsumer.getSmartContractParamsOrThrowAsync( marketBuySwapQuote, { feePercentage: 0.05, feeRecipient, }, ); expect(toAddress).to.deep.equal(contractWrappers.forwarder.address); const { makerAssetFillAmount, feeSignatures, feePercentage, feeRecipient: feeRecipientFromParams, signatures, type, } = params as ForwarderMarketBuySmartContractParams; expect(type).to.deep.equal(MarketOperation.Buy); expect(feeRecipientFromParams).to.deep.equal(feeRecipient); expect(makerAssetFillAmount).to.bignumber.equal( (marketBuySwapQuote as MarketBuySwapQuote).makerAssetFillAmount, ); const orderSignatures = marketBuySwapQuote.orders.map(order => order.signature); expect(signatures).to.deep.equal(orderSignatures); expect(feePercentage).to.bignumber.equal(new BigNumber(0.05).multipliedBy(ONE_ETH_IN_WEI)); expect(feeSignatures).to.deep.equal([]); }); }); }); describe('getCalldataOrThrow', () => { describe('validation', () => { it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => { const invalidSignedOrders = await getSignedOrdersWithNoFeesAsync( provider, makerAssetData, takerAssetData, makerAddress, takerAddress, FILLABLE_AMOUNTS, ); const invalidSwapQuote = getFullyFillableSwapQuoteWithNoFees( makerAssetData, takerAssetData, invalidSignedOrders, MARKET_OPERATION, ); expect(swapQuoteConsumer.getCalldataOrThrowAsync(invalidSwapQuote, {})).to.be.rejectedWith( `Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`, ); }); }); describe('valid swap quote', async () => { it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => { let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); const { calldataHexString, toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync( marketSellSwapQuote, {}, ); expect(toAddress).to.deep.equal(contractWrappers.forwarder.address); await web3Wrapper.sendTransactionAsync({ from: takerAddress, to: toAddress, data: calldataHexString, value: marketSellSwapQuote.worstCaseQuoteInfo.totalTakerTokenAmount, gas: 4000000, }); makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); expect(makerBalance).to.bignumber.equal(new BigNumber(0.5).multipliedBy(ONE_ETH_IN_WEI)); expect(takerBalance).to.bignumber.equal(new BigNumber(9.5).multipliedBy(ONE_ETH_IN_WEI)); }); it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => { let makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); let takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); expect(makerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); expect(takerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); const { calldataHexString, toAddress } = await swapQuoteConsumer.getCalldataOrThrowAsync( marketBuySwapQuote, {}, ); expect(toAddress).to.deep.equal(contractAddresses.forwarder); await web3Wrapper.sendTransactionAsync({ from: takerAddress, to: toAddress, data: calldataHexString, value: marketBuySwapQuote.worstCaseQuoteInfo.totalTakerTokenAmount, gas: 4000000, }); makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress); takerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, takerAddress); expect(takerBalance).to.bignumber.equal(new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI)); expect(makerBalance).to.bignumber.equal(constants.ZERO_AMOUNT); }); // TODO(david) finish testing for affiliate fees calldata output // it('provide correct and optimized calldata options with affiliate fees for a marketSell SwapQuote', async () => { // }); // it('provide correct and optimized calldata options with affiliate fees for a marketBuy SwapQuote', async () => { // }); }); }); });