import { LibMathRevertErrors } from '@0x/contracts-exchange-libs'; import { blockchainTests, constants, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils'; import { AssetProxyId, RevertReason } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { DydxBridgeActionType, DydxBridgeData, dydxBridgeDataEncoder } from '../src/dydx_bridge_encoder'; import { ERC20BridgeProxyContract, IAssetDataContract } from '../src/wrappers'; import { artifacts } from './artifacts'; import { TestDydxBridgeContract, TestDydxBridgeEvents } from './wrappers'; blockchainTests.resets('DydxBridge unit tests', env => { const defaultAccountNumber = new BigNumber(1); const marketId = new BigNumber(2); const defaultAmount = new BigNumber(4); const notAuthorized = '0x0000000000000000000000000000000000000001'; const defaultDepositAction = { actionType: DydxBridgeActionType.Deposit, accountIdx: constants.ZERO_AMOUNT, marketId, conversionRateNumerator: constants.ZERO_AMOUNT, conversionRateDenominator: constants.ZERO_AMOUNT, }; const defaultWithdrawAction = { actionType: DydxBridgeActionType.Withdraw, accountIdx: constants.ZERO_AMOUNT, marketId, conversionRateNumerator: constants.ZERO_AMOUNT, conversionRateDenominator: constants.ZERO_AMOUNT, }; let testContract: TestDydxBridgeContract; let testProxyContract: ERC20BridgeProxyContract; let assetDataEncoder: IAssetDataContract; let owner: string; let authorized: string; let accountOwner: string; let receiver: string; before(async () => { // Get accounts const accounts = await env.web3Wrapper.getAvailableAddressesAsync(); [owner, authorized, accountOwner, receiver] = accounts; // Deploy dydx bridge testContract = await TestDydxBridgeContract.deployFrom0xArtifactAsync( artifacts.TestDydxBridge, env.provider, env.txDefaults, artifacts, [accountOwner, receiver], ); // Deploy test erc20 bridge proxy testProxyContract = await ERC20BridgeProxyContract.deployFrom0xArtifactAsync( artifacts.ERC20BridgeProxy, env.provider, env.txDefaults, artifacts, ); await testProxyContract.addAuthorizedAddress(authorized).awaitTransactionSuccessAsync({ from: owner }); // Setup asset data encoder assetDataEncoder = new IAssetDataContract(constants.NULL_ADDRESS, env.provider); }); describe('bridgeTransferFrom()', () => { const callBridgeTransferFrom = async ( from: string, to: string, amount: BigNumber, bridgeData: DydxBridgeData, sender: string, ): Promise => { const returnValue = await testContract .bridgeTransferFrom( constants.NULL_ADDRESS, from, to, amount, dydxBridgeDataEncoder.encode({ bridgeData }), ) .callAsync({ from: sender }); return returnValue; }; const executeBridgeTransferFromAndVerifyEvents = async ( from: string, to: string, amount: BigNumber, bridgeData: DydxBridgeData, sender: string, ): Promise => { // Execute transaction. const txReceipt = await testContract .bridgeTransferFrom( constants.NULL_ADDRESS, from, to, amount, dydxBridgeDataEncoder.encode({ bridgeData }), ) .awaitTransactionSuccessAsync({ from: sender }); // Verify `OperateAccount` event. const expectedOperateAccountEvents = []; for (const accountNumber of bridgeData.accountNumbers) { expectedOperateAccountEvents.push({ owner: accountOwner, number: accountNumber, }); } verifyEventsFromLogs(txReceipt.logs, expectedOperateAccountEvents, TestDydxBridgeEvents.OperateAccount); // Verify `OperateAction` event. const weiDenomination = 0; const deltaAmountRef = 0; const expectedOperateActionEvents = []; for (const action of bridgeData.actions) { expectedOperateActionEvents.push({ actionType: action.actionType as number, accountIdx: action.accountIdx, amountSign: action.actionType === DydxBridgeActionType.Deposit ? true : false, amountDenomination: weiDenomination, amountRef: deltaAmountRef, amountValue: action.conversionRateDenominator.gt(0) ? amount .times(action.conversionRateNumerator) .dividedToIntegerBy(action.conversionRateDenominator) : amount, primaryMarketId: marketId, secondaryMarketId: constants.ZERO_AMOUNT, otherAddress: action.actionType === DydxBridgeActionType.Deposit ? from : to, otherAccountId: constants.ZERO_AMOUNT, data: '0x', }); } verifyEventsFromLogs(txReceipt.logs, expectedOperateActionEvents, TestDydxBridgeEvents.OperateAction); }; it('succeeds when calling with zero amount', async () => { const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [defaultDepositAction], }; await executeBridgeTransferFromAndVerifyEvents( accountOwner, receiver, constants.ZERO_AMOUNT, bridgeData, authorized, ); }); it('succeeds when calling with no accounts', async () => { const bridgeData = { accountNumbers: [], actions: [defaultDepositAction], }; await executeBridgeTransferFromAndVerifyEvents( accountOwner, receiver, defaultAmount, bridgeData, authorized, ); }); it('succeeds when calling with no actions', async () => { const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [], }; await executeBridgeTransferFromAndVerifyEvents( accountOwner, receiver, defaultAmount, bridgeData, authorized, ); }); it('succeeds when calling `operate` with the `deposit` action and a single account', async () => { const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [defaultDepositAction], }; await executeBridgeTransferFromAndVerifyEvents( accountOwner, receiver, defaultAmount, bridgeData, authorized, ); }); it('succeeds when calling `operate` with the `deposit` action and multiple accounts', async () => { const bridgeData = { accountNumbers: [defaultAccountNumber, defaultAccountNumber.plus(1)], actions: [defaultDepositAction], }; await executeBridgeTransferFromAndVerifyEvents( accountOwner, receiver, defaultAmount, bridgeData, authorized, ); }); it('succeeds when calling `operate` with the `withdraw` action and a single account', async () => { const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [defaultWithdrawAction], }; await executeBridgeTransferFromAndVerifyEvents( accountOwner, receiver, defaultAmount, bridgeData, authorized, ); }); it('succeeds when calling `operate` with the `withdraw` action and multiple accounts', async () => { const bridgeData = { accountNumbers: [defaultAccountNumber, defaultAccountNumber.plus(1)], actions: [defaultWithdrawAction], }; await executeBridgeTransferFromAndVerifyEvents( accountOwner, receiver, defaultAmount, bridgeData, authorized, ); }); it('succeeds when calling `operate` with the `deposit` action and multiple accounts', async () => { const bridgeData = { accountNumbers: [defaultAccountNumber, defaultAccountNumber.plus(1)], actions: [defaultWithdrawAction, defaultDepositAction], }; await executeBridgeTransferFromAndVerifyEvents( accountOwner, receiver, defaultAmount, bridgeData, authorized, ); }); it('succeeds when calling `operate` with multiple actions under a single account', async () => { const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [defaultWithdrawAction, defaultDepositAction], }; await executeBridgeTransferFromAndVerifyEvents( accountOwner, receiver, defaultAmount, bridgeData, authorized, ); }); it('succeeds when scaling the `amount` to deposit', async () => { const conversionRateNumerator = new BigNumber(1); const conversionRateDenominator = new BigNumber(2); const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [ defaultWithdrawAction, { ...defaultDepositAction, conversionRateNumerator, conversionRateDenominator, }, ], }; await executeBridgeTransferFromAndVerifyEvents( accountOwner, receiver, defaultAmount, bridgeData, authorized, ); }); it('succeeds when scaling the `amount` to withdraw', async () => { const conversionRateNumerator = new BigNumber(1); const conversionRateDenominator = new BigNumber(2); const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [ defaultDepositAction, { ...defaultWithdrawAction, conversionRateNumerator, conversionRateDenominator, }, ], }; await executeBridgeTransferFromAndVerifyEvents( accountOwner, receiver, defaultAmount, bridgeData, authorized, ); }); it('reverts if not called by the ERC20 Bridge Proxy', async () => { const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [defaultDepositAction], }; const callBridgeTransferFromPromise = callBridgeTransferFrom( accountOwner, receiver, defaultAmount, bridgeData, notAuthorized, ); const expectedError = RevertReason.DydxBridgeOnlyCallableByErc20BridgeProxy; return expect(callBridgeTransferFromPromise).to.revertWith(expectedError); }); it('should return magic bytes if call succeeds', async () => { const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [defaultDepositAction], }; const returnValue = await callBridgeTransferFrom( accountOwner, receiver, defaultAmount, bridgeData, authorized, ); expect(returnValue).to.equal(AssetProxyId.ERC20Bridge); }); it('should revert when `Operate` reverts', async () => { // Set revert flag. await testContract.setRevertOnOperate(true).awaitTransactionSuccessAsync(); // Execute transfer. const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [defaultDepositAction], }; const tx = callBridgeTransferFrom(accountOwner, receiver, defaultAmount, bridgeData, authorized); const expectedError = 'TestDydxBridge/SHOULD_REVERT_ON_OPERATE'; return expect(tx).to.revertWith(expectedError); }); it('should revert when there is a rounding error', async () => { // Setup a rounding error const conversionRateNumerator = new BigNumber(5318); const conversionRateDenominator = new BigNumber(47958); const amount = new BigNumber(9000); const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [ defaultDepositAction, { ...defaultWithdrawAction, conversionRateNumerator, conversionRateDenominator, }, ], }; // Execute transfer and assert error. const tx = callBridgeTransferFrom(accountOwner, receiver, amount, bridgeData, authorized); const expectedError = new LibMathRevertErrors.RoundingError( conversionRateNumerator, conversionRateDenominator, amount, ); return expect(tx).to.revertWith(expectedError); }); }); describe('ERC20BridgeProxy.transferFrom()', () => { const bridgeData = { accountNumbers: [defaultAccountNumber], actions: [defaultWithdrawAction], }; let assetData: string; before(async () => { const testTokenAddress = await testContract.getTestToken().callAsync(); assetData = assetDataEncoder .ERC20Bridge(testTokenAddress, testContract.address, dydxBridgeDataEncoder.encode({ bridgeData })) .getABIEncodedTransactionData(); }); it('should succeed if `bridgeTransferFrom` succeeds', async () => { await testProxyContract .transferFrom(assetData, accountOwner, receiver, defaultAmount) .awaitTransactionSuccessAsync({ from: authorized }); }); it('should revert if `bridgeTransferFrom` reverts', async () => { // Set revert flag. await testContract.setRevertOnOperate(true).awaitTransactionSuccessAsync(); const tx = testProxyContract .transferFrom(assetData, accountOwner, receiver, defaultAmount) .awaitTransactionSuccessAsync({ from: authorized }); const expectedError = 'TestDydxBridge/SHOULD_REVERT_ON_OPERATE'; return expect(tx).to.revertWith(expectedError); }); }); });