import { DydxBridgeActionType, dydxBridgeDataEncoder, encodeERC20AssetData, encodeERC20BridgeAssetData, TestDydxBridgeContract, } from '@0x/contracts-asset-proxy'; import { DummyERC20TokenContract } from '@0x/contracts-erc20'; import { LibMathRevertErrors } from '@0x/contracts-exchange-libs'; import { blockchainTests, constants, describe, expect, toBaseUnitAmount } from '@0x/contracts-test-utils'; import { SignedOrder } from '@0x/order-utils'; import { BigNumber, RevertError } from '@0x/utils'; import { DecodedLogArgs, LogWithDecodedArgs } from 'ethereum-types'; import * as _ from 'lodash'; import { deployDydxBridgeAsync } from '../bridges/deploy_dydx_bridge'; import { Actor } from '../framework/actors/base'; import { Maker } from '../framework/actors/maker'; import { Taker } from '../framework/actors/taker'; import { DeploymentManager } from '../framework/deployment_manager'; blockchainTests.resets('Exchange fills dydx orders', env => { let testContract: TestDydxBridgeContract; let makerToken: DummyERC20TokenContract; let takerToken: DummyERC20TokenContract; const marketId = new BigNumber(3); const dydxConversionRateNumerator = toBaseUnitAmount(6); const dydxConversionRateDenominator = toBaseUnitAmount(1); let maker: Maker; let taker: Taker; let orderConfig: Partial; let dydxBridgeProxyAssetData: string; let deployment: DeploymentManager; let testTokenAddress: string; const defaultDepositAction = { actionType: DydxBridgeActionType.Deposit as number, accountId: constants.ZERO_AMOUNT, marketId, conversionRateNumerator: dydxConversionRateNumerator, conversionRateDenominator: dydxConversionRateDenominator, }; const defaultWithdrawAction = { actionType: DydxBridgeActionType.Withdraw as number, accountId: constants.ZERO_AMOUNT, marketId, conversionRateNumerator: constants.ZERO_AMOUNT, conversionRateDenominator: constants.ZERO_AMOUNT, }; const bridgeData = { accountNumbers: [new BigNumber(0)], actions: [defaultDepositAction, defaultWithdrawAction], }; before(async () => { // Deploy contracts deployment = await DeploymentManager.deployAsync(env, { numErc20TokensToDeploy: 2, }); testContract = await deployDydxBridgeAsync(deployment, env); const encodedBridgeData = dydxBridgeDataEncoder.encode({ bridgeData }); testTokenAddress = await testContract.getTestToken().callAsync(); dydxBridgeProxyAssetData = encodeERC20BridgeAssetData( testTokenAddress, testContract.address, encodedBridgeData, ); [makerToken, takerToken] = deployment.tokens.erc20; // Configure default order parameters. orderConfig = { makerAssetAmount: toBaseUnitAmount(1), takerAssetAmount: toBaseUnitAmount(1), makerAssetData: encodeERC20AssetData(makerToken.address), takerAssetData: encodeERC20AssetData(takerToken.address), // Not important for this test. feeRecipientAddress: constants.NULL_ADDRESS, makerFeeAssetData: encodeERC20AssetData(makerToken.address), takerFeeAssetData: encodeERC20AssetData(takerToken.address), makerFee: toBaseUnitAmount(1), takerFee: toBaseUnitAmount(1), }; // Configure maker. maker = new Maker({ name: 'Maker', deployment, orderConfig, }); await maker.configureERC20TokenAsync(makerToken, deployment.assetProxies.erc20Proxy.address); // Configure taker. taker = new Taker({ name: 'Taker', deployment, }); await taker.configureERC20TokenAsync(takerToken, deployment.assetProxies.erc20Proxy.address); }); after(async () => { Actor.reset(); }); describe('fillOrder', () => { interface DydxFillResults { makerAssetFilledAmount: BigNumber; takerAssetFilledAmount: BigNumber; makerFeePaid: BigNumber; takerFeePaid: BigNumber; amountDepositedIntoDydx: BigNumber; amountWithdrawnFromDydx: BigNumber; } const fillOrder = async ( signedOrder: SignedOrder, customTakerAssetFillAmount?: BigNumber, ): Promise => { // Fill order const takerAssetFillAmount = customTakerAssetFillAmount !== undefined ? customTakerAssetFillAmount : signedOrder.takerAssetAmount; const tx = await taker.fillOrderAsync(signedOrder, takerAssetFillAmount); // Extract values from fill event. // tslint:disable no-unnecessary-type-assertion const fillEvent = _.find(tx.logs, log => { return (log as any).event === 'Fill'; }) as LogWithDecodedArgs; // Extract amount deposited into dydx from maker. const dydxDepositEvent = _.find(tx.logs, log => { return ( (log as any).event === 'OperateAction' && (log as any).args.actionType === DydxBridgeActionType.Deposit ); }) as LogWithDecodedArgs; // Extract amount withdrawn from dydx to taker. const dydxWithdrawEvent = _.find(tx.logs, log => { return ( (log as any).event === 'OperateAction' && (log as any).args.actionType === DydxBridgeActionType.Withdraw ); }) as LogWithDecodedArgs; // Return values of interest for assertions. return { makerAssetFilledAmount: fillEvent.args.makerAssetFilledAmount, takerAssetFilledAmount: fillEvent.args.takerAssetFilledAmount, makerFeePaid: fillEvent.args.makerFeePaid, takerFeePaid: fillEvent.args.takerFeePaid, amountDepositedIntoDydx: dydxDepositEvent.args.amountValue, amountWithdrawnFromDydx: dydxWithdrawEvent.args.amountValue, }; }; it('should successfully fill a dydx order (DydxBridge used in makerAssetData)', async () => { const signedOrder = await maker.signOrderAsync({ // Invert the dydx conversion rate when using the bridge in the makerAssetData. makerAssetData: dydxBridgeProxyAssetData, makerAssetAmount: dydxConversionRateDenominator, takerAssetAmount: dydxConversionRateNumerator, }); const dydxFillResults = await fillOrder(signedOrder); expect( dydxFillResults.makerAssetFilledAmount, 'makerAssetFilledAmount should equal amountWithdrawnFromDydx', ).to.bignumber.equal(dydxFillResults.amountWithdrawnFromDydx); expect( dydxFillResults.takerAssetFilledAmount, 'takerAssetFilledAmount should equal amountDepositedIntoDydx', ).to.bignumber.equal(dydxFillResults.amountDepositedIntoDydx); }); it('should successfully fill a dydx order (DydxBridge used in takerAssetData)', async () => { const signedOrder = await maker.signOrderAsync({ // Match the dydx conversion rate when using the bridge in the takerAssetData. takerAssetData: dydxBridgeProxyAssetData, makerAssetAmount: dydxConversionRateNumerator, takerAssetAmount: dydxConversionRateDenominator, }); const dydxFillResults = await fillOrder(signedOrder); expect( dydxFillResults.makerAssetFilledAmount, 'makerAssetFilledAmount should equal amountDepositedIntoDydx', ).to.bignumber.equal(dydxFillResults.amountDepositedIntoDydx); expect( dydxFillResults.takerAssetFilledAmount, 'takerAssetFilledAmount should equal amountWithdrawnFromDydx', ).to.bignumber.equal(dydxFillResults.amountWithdrawnFromDydx); }); it('should successfully fill a dydx order (DydxBridge used in makerFeeAssetData)', async () => { const signedOrder = await maker.signOrderAsync({ // Invert the dydx conversion rate when using the bridge in the makerFeeAssetData. makerFeeAssetData: dydxBridgeProxyAssetData, makerFee: dydxConversionRateDenominator, takerFee: dydxConversionRateNumerator, }); const dydxFillResults = await fillOrder(signedOrder); expect( dydxFillResults.makerFeePaid, 'makerFeePaid should equal amountWithdrawnFromDydx', ).to.bignumber.equal(dydxFillResults.amountWithdrawnFromDydx); expect( dydxFillResults.takerFeePaid, 'takerFeePaid should equal amountDepositedIntoDydx', ).to.bignumber.equal(dydxFillResults.amountDepositedIntoDydx); }); it('should successfully fill a dydx order (DydxBridge used in takerFeeAssetData)', async () => { const signedOrder = await maker.signOrderAsync({ // Match the dydx conversion rate when using the bridge in the takerFeeAssetData. takerFeeAssetData: dydxBridgeProxyAssetData, makerFee: dydxConversionRateNumerator, takerFee: dydxConversionRateDenominator, }); const dydxFillResults = await fillOrder(signedOrder); expect( dydxFillResults.makerFeePaid, 'makerFeePaid should equal amountDepositedIntoDydx', ).to.bignumber.equal(dydxFillResults.amountDepositedIntoDydx); expect( dydxFillResults.takerFeePaid, 'takerFeePaid should equal amountWithdrawnFromDydx', ).to.bignumber.equal(dydxFillResults.amountWithdrawnFromDydx); }); it('should partially fill a dydx order (DydxBridge used in makerAssetData)', async () => { const signedOrder = await maker.signOrderAsync({ // Invert the dydx conversion rate when using the bridge in the makerAssetData. makerAssetData: dydxBridgeProxyAssetData, makerAssetAmount: dydxConversionRateDenominator, takerAssetAmount: dydxConversionRateNumerator, }); const dydxFillResults = await fillOrder(signedOrder, signedOrder.takerAssetAmount.div(2)); expect( dydxFillResults.makerAssetFilledAmount, 'makerAssetFilledAmount should equal amountWithdrawnFromDydx', ).to.bignumber.equal(dydxFillResults.amountWithdrawnFromDydx); expect( dydxFillResults.takerAssetFilledAmount, 'takerAssetFilledAmount should equal amountDepositedIntoDydx', ).to.bignumber.equal(dydxFillResults.amountDepositedIntoDydx); }); it('should revert in DydxBridge when there is a rounding error', async () => { // This order will not trigger the rounding error in the Exchange, // but will trigger one in the DydxBridge. const badDepositAction = { ...defaultDepositAction, conversionRateNumerator: new BigNumber(5318), conversionRateDenominator: new BigNumber(47958), }; const badBridgeData = { accountNumbers: [new BigNumber(0)], actions: [badDepositAction], }; const encodedBridgeData = dydxBridgeDataEncoder.encode({ bridgeData: badBridgeData }); const badDydxBridgeProxyAssetData = encodeERC20BridgeAssetData( testTokenAddress, testContract.address, encodedBridgeData, ); const signedOrder = await maker.signOrderAsync({ makerAssetData: badDydxBridgeProxyAssetData, makerAssetAmount: badDepositAction.conversionRateDenominator, takerAssetAmount: badDepositAction.conversionRateNumerator, }); const takerAssetFillAmount = new BigNumber(998); // Compute expected error. const target = takerAssetFillAmount .multipliedBy(signedOrder.makerAssetAmount) .dividedToIntegerBy(signedOrder.takerAssetAmount); const expectedAssetProxyError = new LibMathRevertErrors.RoundingError( signedOrder.takerAssetAmount, signedOrder.makerAssetAmount, target, ); // Call fillOrder and assert the rounding error generated by the DydxBridge. const txPromise = taker.fillOrderAsync(signedOrder, takerAssetFillAmount); let assetProxyError; try { await txPromise.catch(); } catch (e) { assetProxyError = RevertError.decode(e.values.errorData); } expect(assetProxyError).to.deep.equal(expectedAssetProxyError); }); }); });