diff --git a/contracts/integrations/test/bridges/abi/dydxEvents.ts b/contracts/integrations/test/bridges/abi/dydxEvents.ts new file mode 100644 index 0000000000..d41d24d27c --- /dev/null +++ b/contracts/integrations/test/bridges/abi/dydxEvents.ts @@ -0,0 +1,1002 @@ +// tslint:disable max-file-line-count +export const dydxEvents = { + contractName: 'Events', + abi: [ + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'market', + type: 'uint256', + }, + { + components: [ + { + name: 'borrow', + type: 'uint96', + }, + { + name: 'supply', + type: 'uint96', + }, + { + name: 'lastUpdate', + type: 'uint32', + }, + ], + indexed: false, + name: 'index', + type: 'tuple', + }, + ], + name: 'LogIndexUpdate', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + name: 'sender', + type: 'address', + }, + ], + name: 'LogOperation', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'accountOwner', + type: 'address', + }, + { + indexed: false, + name: 'accountNumber', + type: 'uint256', + }, + { + indexed: false, + name: 'market', + type: 'uint256', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'update', + type: 'tuple', + }, + { + indexed: false, + name: 'from', + type: 'address', + }, + ], + name: 'LogDeposit', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'accountOwner', + type: 'address', + }, + { + indexed: false, + name: 'accountNumber', + type: 'uint256', + }, + { + indexed: false, + name: 'market', + type: 'uint256', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'update', + type: 'tuple', + }, + { + indexed: false, + name: 'to', + type: 'address', + }, + ], + name: 'LogWithdraw', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'accountOneOwner', + type: 'address', + }, + { + indexed: false, + name: 'accountOneNumber', + type: 'uint256', + }, + { + indexed: true, + name: 'accountTwoOwner', + type: 'address', + }, + { + indexed: false, + name: 'accountTwoNumber', + type: 'uint256', + }, + { + indexed: false, + name: 'market', + type: 'uint256', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'updateOne', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'updateTwo', + type: 'tuple', + }, + ], + name: 'LogTransfer', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'accountOwner', + type: 'address', + }, + { + indexed: false, + name: 'accountNumber', + type: 'uint256', + }, + { + indexed: false, + name: 'takerMarket', + type: 'uint256', + }, + { + indexed: false, + name: 'makerMarket', + type: 'uint256', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'takerUpdate', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'makerUpdate', + type: 'tuple', + }, + { + indexed: false, + name: 'exchangeWrapper', + type: 'address', + }, + ], + name: 'LogBuy', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'accountOwner', + type: 'address', + }, + { + indexed: false, + name: 'accountNumber', + type: 'uint256', + }, + { + indexed: false, + name: 'takerMarket', + type: 'uint256', + }, + { + indexed: false, + name: 'makerMarket', + type: 'uint256', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'takerUpdate', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'makerUpdate', + type: 'tuple', + }, + { + indexed: false, + name: 'exchangeWrapper', + type: 'address', + }, + ], + name: 'LogSell', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'takerAccountOwner', + type: 'address', + }, + { + indexed: false, + name: 'takerAccountNumber', + type: 'uint256', + }, + { + indexed: true, + name: 'makerAccountOwner', + type: 'address', + }, + { + indexed: false, + name: 'makerAccountNumber', + type: 'uint256', + }, + { + indexed: false, + name: 'inputMarket', + type: 'uint256', + }, + { + indexed: false, + name: 'outputMarket', + type: 'uint256', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'takerInputUpdate', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'takerOutputUpdate', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'makerInputUpdate', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'makerOutputUpdate', + type: 'tuple', + }, + { + indexed: false, + name: 'autoTrader', + type: 'address', + }, + ], + name: 'LogTrade', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'accountOwner', + type: 'address', + }, + { + indexed: false, + name: 'accountNumber', + type: 'uint256', + }, + { + indexed: false, + name: 'callee', + type: 'address', + }, + ], + name: 'LogCall', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'solidAccountOwner', + type: 'address', + }, + { + indexed: false, + name: 'solidAccountNumber', + type: 'uint256', + }, + { + indexed: true, + name: 'liquidAccountOwner', + type: 'address', + }, + { + indexed: false, + name: 'liquidAccountNumber', + type: 'uint256', + }, + { + indexed: false, + name: 'heldMarket', + type: 'uint256', + }, + { + indexed: false, + name: 'owedMarket', + type: 'uint256', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'solidHeldUpdate', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'solidOwedUpdate', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'liquidHeldUpdate', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'liquidOwedUpdate', + type: 'tuple', + }, + ], + name: 'LogLiquidate', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'solidAccountOwner', + type: 'address', + }, + { + indexed: false, + name: 'solidAccountNumber', + type: 'uint256', + }, + { + indexed: true, + name: 'vaporAccountOwner', + type: 'address', + }, + { + indexed: false, + name: 'vaporAccountNumber', + type: 'uint256', + }, + { + indexed: false, + name: 'heldMarket', + type: 'uint256', + }, + { + indexed: false, + name: 'owedMarket', + type: 'uint256', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'solidHeldUpdate', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'solidOwedUpdate', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'deltaWei', + type: 'tuple', + }, + { + components: [ + { + name: 'sign', + type: 'bool', + }, + { + name: 'value', + type: 'uint128', + }, + ], + name: 'newPar', + type: 'tuple', + }, + ], + indexed: false, + name: 'vaporOwedUpdate', + type: 'tuple', + }, + ], + name: 'LogVaporize', + type: 'event', + }, + ], +}; diff --git a/contracts/integrations/test/bridges/deploy_dydx_bridge.ts b/contracts/integrations/test/bridges/deploy_dydx_bridge.ts new file mode 100644 index 0000000000..de8f87f11d --- /dev/null +++ b/contracts/integrations/test/bridges/deploy_dydx_bridge.ts @@ -0,0 +1,20 @@ +import { artifacts as assetProxyArtifacts, TestDydxBridgeContract } from '@0x/contracts-asset-proxy'; +import { BlockchainTestsEnvironment } from '@0x/contracts-test-utils'; + +import { DeploymentManager } from '../framework/deployment_manager'; + +/** + * Deploys test DydxBridge contract configured to work alongside the provided `deployment`. + */ +export async function deployDydxBridgeAsync( + deployment: DeploymentManager, + environment: BlockchainTestsEnvironment, +): Promise { + const dydxBridge = await TestDydxBridgeContract.deployFrom0xArtifactAsync( + assetProxyArtifacts.TestDydxBridge, + environment.provider, + deployment.txDefaults, + assetProxyArtifacts, + ); + return dydxBridge; +} diff --git a/contracts/integrations/test/exchange/fill_dydx_order_test.ts b/contracts/integrations/test/exchange/fill_dydx_order_test.ts new file mode 100644 index 0000000000..277b1e5af7 --- /dev/null +++ b/contracts/integrations/test/exchange/fill_dydx_order_test.ts @@ -0,0 +1,133 @@ +import { DydxBridgeActionType, dydxBridgeDataEncoder, TestDydxBridgeContract } from '@0x/contracts-asset-proxy'; +import { DummyERC20TokenContract } from '@0x/contracts-erc20'; +import { blockchainTests, constants, describe, expect, toBaseUnitAmount } from '@0x/contracts-test-utils'; +import { BigNumber } from '@0x/utils'; +import { DecodedLogArgs, LogEntry, 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 makerAssetAmount = toBaseUnitAmount(6); + const takerAssetAmount = toBaseUnitAmount(1); + let maker: Maker; + let taker: Taker; + const defaultDepositAction = { + actionType: DydxBridgeActionType.Deposit as number, + accountId: constants.ZERO_AMOUNT, + marketId, + // The bridge is passed the `makerAssetFillAmount` and we + // want to compute the input `takerAssetFillAmount` + // => multiply by `takerAssetAmount` / `makerAssetAmount`. + conversionRateNumerator: takerAssetAmount, + conversionRateDenominator: makerAssetAmount, + }; + const defaultWithdrawAction = { + actionType: DydxBridgeActionType.Withdraw as number, + accountId: constants.ZERO_AMOUNT, + marketId, + conversionRateNumerator: constants.ZERO_AMOUNT, + conversionRateDenominator: constants.ZERO_AMOUNT, + }; + const defaultBridgeData = { + accountNumbers: [new BigNumber(0)], + actions: [defaultDepositAction, defaultWithdrawAction], + }; + + before(async () => { + // Deploy contracts + const deployment = await DeploymentManager.deployAsync(env, { + numErc20TokensToDeploy: 2, + }); + testContract = await deployDydxBridgeAsync(deployment, env); + const bridgeData = dydxBridgeDataEncoder.encode({defaultBridgeData}); + const bridgeProxyAssetData = deployment.assetDataEncoder + .ERC20Bridge(testContract.address, testContract.address, bridgeData) + .getABIEncodedTransactionData(); + [makerToken, takerToken] = deployment.tokens.erc20; + + // Configure Maker & Taker. + const orderConfig = { + makerAssetAmount, + takerAssetAmount, + makerAssetData: bridgeProxyAssetData, + takerAssetData: deployment.assetDataEncoder.ERC20Token(takerToken.address).getABIEncodedTransactionData(), + // Not important for this test. + feeRecipientAddress: constants.NULL_ADDRESS, + makerFeeAssetData: deployment.assetDataEncoder + .ERC20Token(makerToken.address) + .getABIEncodedTransactionData(), + takerFeeAssetData: deployment.assetDataEncoder + .ERC20Token(takerToken.address) + .getABIEncodedTransactionData(), + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }; + maker = new Maker({ + name: 'Maker', + deployment, + orderConfig, + }); + taker = new Taker({ + name: 'Taker', + deployment, + }); + await taker.configureERC20TokenAsync(takerToken, deployment.assetProxies.erc20Proxy.address); + }); + + after(async () => { + Actor.reset(); + }); + + describe('fillOrder', () => { + const verifyEvents = (logs: Array | LogEntry>): void => { + // Extract values from fill event. + // tslint:disable no-unnecessary-type-assertion + const fillEvent = _.find(logs, log => { + return (log as any).event === 'Fill'; + }) as LogWithDecodedArgs; + const makerAssetFilledAmount = fillEvent.args.makerAssetFilledAmount; + const takerAssetFilledAmount = fillEvent.args.takerAssetFilledAmount; + + // Extract amount deposited into dydx from maker. + const dydxDepositEvent = _.find(logs, log => { + return ( + (log as any).event === 'OperateAction' && + (log as any).args.actionType === DydxBridgeActionType.Deposit + ); + }) as LogWithDecodedArgs; + const amountDepositedIntoDydx = dydxDepositEvent.args.amountValue; + + // Extract amount withdrawn from dydx to taker. + const dydxWithdrawEvent = _.find(logs, log => { + return ( + (log as any).event === 'OperateAction' && + (log as any).args.actionType === DydxBridgeActionType.Withdraw + ); + }) as LogWithDecodedArgs; + const amountWithdrawnFromDydx = dydxWithdrawEvent.args.amountValue; + + // Assert fill amounts match amounts deposited/withdrawn from dydx. + expect(makerAssetFilledAmount).to.bignumber.equal(amountWithdrawnFromDydx); + expect(takerAssetFilledAmount).to.bignumber.equal(amountDepositedIntoDydx); + }; + it('should successfully fill a dydx order', async () => { + const signedOrder = await maker.signOrderAsync(); + const tx = await taker.fillOrderAsync(signedOrder, signedOrder.takerAssetAmount); + verifyEvents(tx.logs); + }); + it('should partially fill a dydx order', async () => { + const signedOrder = await maker.signOrderAsync(); + const tx = await taker.fillOrderAsync(signedOrder, signedOrder.takerAssetAmount.div(2)); + verifyEvents(tx.logs); + }); + }); +}); diff --git a/contracts/integrations/test/framework/deployment_manager.ts b/contracts/integrations/test/framework/deployment_manager.ts index f7e4012166..a2e32a165e 100644 --- a/contracts/integrations/test/framework/deployment_manager.ts +++ b/contracts/integrations/test/framework/deployment_manager.ts @@ -145,7 +145,7 @@ export class DeploymentManager { exchangeArtifacts.Exchange, environment.provider, txDefaults, - { ...ERC20Artifacts, ...exchangeArtifacts, ...stakingArtifacts }, + { ...ERC20Artifacts, ...exchangeArtifacts, ...stakingArtifacts, ...assetProxyArtifacts }, new BigNumber(chainId), ); const governor = await ZeroExGovernorContract.deployFrom0xArtifactAsync(