355 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			355 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { ContractTxFunctionObj } from '@0x/contract-wrappers';
 | |
| import {
 | |
|     blockchainTests,
 | |
|     constants,
 | |
|     expect,
 | |
|     filterLogsToArguments,
 | |
|     getRandomInteger,
 | |
|     randomAddress,
 | |
|     shortZip,
 | |
| } from '@0x/contracts-test-utils';
 | |
| import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils';
 | |
| import { DecodedLogs } from 'ethereum-types';
 | |
| import * as _ from 'lodash';
 | |
| 
 | |
| import { DexForwarderBridgeCall, dexForwarderBridgeDataEncoder } from '../src/dex_forwarder_bridge';
 | |
| 
 | |
| import { artifacts } from './artifacts';
 | |
| import {
 | |
|     TestDexForwarderBridgeBridgeTransferFromCalledEventArgs as BtfCalledEventArgs,
 | |
|     TestDexForwarderBridgeContract,
 | |
|     TestDexForwarderBridgeEvents as TestEvents,
 | |
| } from './wrappers';
 | |
| 
 | |
| const { ZERO_AMOUNT } = constants;
 | |
| 
 | |
| blockchainTests.resets('DexForwarderBridge unit tests', env => {
 | |
|     let testContract: TestDexForwarderBridgeContract;
 | |
|     let inputToken: string;
 | |
|     let outputToken: string;
 | |
|     const BRIDGE_SUCCESS = '0xdc1600f3';
 | |
|     const BRIDGE_FAILURE = '0xffffffff';
 | |
|     const BRIDGE_REVERT_ERROR = 'oopsie';
 | |
|     const NOT_AUTHORIZED_REVERT = 'DexForwarderBridge/SENDER_NOT_AUTHORIZED';
 | |
|     const DEFAULTS = {
 | |
|         toAddress: randomAddress(),
 | |
|     };
 | |
| 
 | |
|     before(async () => {
 | |
|         testContract = await TestDexForwarderBridgeContract.deployFrom0xArtifactAsync(
 | |
|             artifacts.TestDexForwarderBridge,
 | |
|             env.provider,
 | |
|             env.txDefaults,
 | |
|             artifacts,
 | |
|         );
 | |
|         // Create test tokens.
 | |
|         [inputToken, outputToken] = [
 | |
|             await callAndTransactAsync(testContract.createToken()),
 | |
|             await callAndTransactAsync(testContract.createToken()),
 | |
|         ];
 | |
|         await callAndTransactAsync(testContract.setAuthorized(env.txDefaults.from as string));
 | |
|     });
 | |
| 
 | |
|     async function callAndTransactAsync<TResult>(fnCall: ContractTxFunctionObj<TResult>): Promise<TResult> {
 | |
|         const result = await fnCall.callAsync();
 | |
|         await fnCall.awaitTransactionSuccessAsync({}, { shouldValidate: false });
 | |
|         return result;
 | |
|     }
 | |
| 
 | |
|     function getRandomBridgeCall(
 | |
|         bridgeAddress: string,
 | |
|         fields: Partial<DexForwarderBridgeCall> = {},
 | |
|     ): DexForwarderBridgeCall {
 | |
|         return {
 | |
|             target: bridgeAddress,
 | |
|             inputTokenAmount: getRandomInteger(1, '100e18'),
 | |
|             outputTokenAmount: getRandomInteger(1, '100e18'),
 | |
|             bridgeData: hexUtils.leftPad(inputToken),
 | |
|             ...fields,
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     describe('bridgeTransferFrom()', () => {
 | |
|         let goodBridgeCalls: DexForwarderBridgeCall[];
 | |
|         let revertingBridgeCall: DexForwarderBridgeCall;
 | |
|         let failingBridgeCall: DexForwarderBridgeCall;
 | |
|         let allBridgeCalls: DexForwarderBridgeCall[];
 | |
|         let totalFillableOutputAmount: BigNumber;
 | |
|         let totalFillableInputAmount: BigNumber;
 | |
|         let recipientOutputBalance: BigNumber;
 | |
| 
 | |
|         beforeEach(async () => {
 | |
|             goodBridgeCalls = [];
 | |
|             for (let i = 0; i < 4; ++i) {
 | |
|                 goodBridgeCalls.push(await createBridgeCallAsync({ returnCode: BRIDGE_SUCCESS }));
 | |
|             }
 | |
|             revertingBridgeCall = await createBridgeCallAsync({ revertError: BRIDGE_REVERT_ERROR });
 | |
|             failingBridgeCall = await createBridgeCallAsync({ returnCode: BRIDGE_FAILURE });
 | |
|             allBridgeCalls = _.shuffle([failingBridgeCall, revertingBridgeCall, ...goodBridgeCalls]);
 | |
| 
 | |
|             totalFillableInputAmount = BigNumber.sum(...goodBridgeCalls.map(c => c.inputTokenAmount));
 | |
|             totalFillableOutputAmount = BigNumber.sum(...goodBridgeCalls.map(c => c.outputTokenAmount));
 | |
| 
 | |
|             // Grant the taker some output tokens.
 | |
|             await testContract.setTokenBalance(
 | |
|                 outputToken,
 | |
|                 DEFAULTS.toAddress,
 | |
|                 (recipientOutputBalance = getRandomInteger(1, '100e18')),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         async function setForwarderInputBalanceAsync(amount: BigNumber): Promise<void> {
 | |
|             await testContract
 | |
|                 .setTokenBalance(inputToken, testContract.address, amount)
 | |
|                 .awaitTransactionSuccessAsync({}, { shouldValidate: false });
 | |
|         }
 | |
| 
 | |
|         async function createBridgeCallAsync(
 | |
|             opts: Partial<{
 | |
|                 returnCode: string;
 | |
|                 revertError: string;
 | |
|                 callFields: Partial<DexForwarderBridgeCall>;
 | |
|                 outputFillAmount: BigNumber;
 | |
|             }>,
 | |
|         ): Promise<DexForwarderBridgeCall> {
 | |
|             const { returnCode, revertError, callFields, outputFillAmount } = {
 | |
|                 returnCode: BRIDGE_SUCCESS,
 | |
|                 revertError: '',
 | |
|                 ...opts,
 | |
|             };
 | |
|             const bridge = await callAndTransactAsync(testContract.createBridge(returnCode, revertError));
 | |
|             const call = getRandomBridgeCall(bridge, callFields);
 | |
|             await testContract
 | |
|                 .setBridgeTransferAmount(call.target, outputFillAmount || call.outputTokenAmount)
 | |
|                 .awaitTransactionSuccessAsync({}, { shouldValidate: false });
 | |
|             return call;
 | |
|         }
 | |
| 
 | |
|         async function callBridgeTransferFromAsync(opts: {
 | |
|             bridgeData: string;
 | |
|             sellAmount?: BigNumber;
 | |
|             buyAmount?: BigNumber;
 | |
|         }): Promise<DecodedLogs> {
 | |
|             // Fund the forwarder with input tokens to sell.
 | |
|             await setForwarderInputBalanceAsync(opts.sellAmount || totalFillableInputAmount);
 | |
|             const call = testContract.bridgeTransferFrom(
 | |
|                 outputToken,
 | |
|                 testContract.address,
 | |
|                 DEFAULTS.toAddress,
 | |
|                 opts.buyAmount || totalFillableOutputAmount,
 | |
|                 opts.bridgeData,
 | |
|             );
 | |
|             const returnCode = await call.callAsync();
 | |
|             if (returnCode !== BRIDGE_SUCCESS) {
 | |
|                 throw new Error('Expected BRIDGE_SUCCESS');
 | |
|             }
 | |
|             const receipt = await call.awaitTransactionSuccessAsync({}, { shouldValidate: false });
 | |
|             // tslint:disable-next-line: no-unnecessary-type-assertion
 | |
|             return receipt.logs as DecodedLogs;
 | |
|         }
 | |
| 
 | |
|         it('succeeds with no bridge calls and no input balance', async () => {
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls: [],
 | |
|             });
 | |
|             await callBridgeTransferFromAsync({ bridgeData, sellAmount: ZERO_AMOUNT });
 | |
|         });
 | |
| 
 | |
|         it('succeeds with bridge calls and no input balance', async () => {
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls: allBridgeCalls,
 | |
|             });
 | |
|             await callBridgeTransferFromAsync({ bridgeData, sellAmount: ZERO_AMOUNT });
 | |
|         });
 | |
| 
 | |
|         it('succeeds with no bridge calls and an input balance', async () => {
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls: [],
 | |
|             });
 | |
|             await callBridgeTransferFromAsync({
 | |
|                 bridgeData,
 | |
|                 sellAmount: new BigNumber(1),
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         it('succeeds if entire input token balance is not consumed', async () => {
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls: allBridgeCalls,
 | |
|             });
 | |
|             await callBridgeTransferFromAsync({
 | |
|                 bridgeData,
 | |
|                 sellAmount: totalFillableInputAmount.plus(1),
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         it('fails if not authorized', async () => {
 | |
|             const calls = goodBridgeCalls.slice(0, 1);
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls,
 | |
|             });
 | |
|             await callAndTransactAsync(testContract.setAuthorized(NULL_ADDRESS));
 | |
|             return expect(callBridgeTransferFromAsync({ bridgeData, sellAmount: new BigNumber(1) })).to.revertWith(
 | |
|                 NOT_AUTHORIZED_REVERT,
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('succeeds with one bridge call', async () => {
 | |
|             const calls = goodBridgeCalls.slice(0, 1);
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls,
 | |
|             });
 | |
|             await callBridgeTransferFromAsync({ bridgeData, sellAmount: calls[0].inputTokenAmount });
 | |
|         });
 | |
| 
 | |
|         it('succeeds with many bridge calls', async () => {
 | |
|             const calls = goodBridgeCalls;
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls,
 | |
|             });
 | |
|             await callBridgeTransferFromAsync({ bridgeData });
 | |
|         });
 | |
| 
 | |
|         it('swallows a failing bridge call', async () => {
 | |
|             const calls = _.shuffle([...goodBridgeCalls, failingBridgeCall]);
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls,
 | |
|             });
 | |
|             await callBridgeTransferFromAsync({ bridgeData });
 | |
|         });
 | |
| 
 | |
|         it('consumes input tokens for output tokens', async () => {
 | |
|             const calls = allBridgeCalls;
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls,
 | |
|             });
 | |
|             await callBridgeTransferFromAsync({ bridgeData });
 | |
|             const currentBridgeInputBalance = await testContract
 | |
|                 .balanceOf(inputToken, testContract.address)
 | |
|                 .callAsync();
 | |
|             expect(currentBridgeInputBalance).to.bignumber.eq(0);
 | |
|             const currentRecipientOutputBalance = await testContract
 | |
|                 .balanceOf(outputToken, DEFAULTS.toAddress)
 | |
|                 .callAsync();
 | |
|             expect(currentRecipientOutputBalance).to.bignumber.eq(totalFillableOutputAmount);
 | |
|         });
 | |
| 
 | |
|         it("transfers only up to each call's input amount to each bridge", async () => {
 | |
|             const calls = goodBridgeCalls;
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls,
 | |
|             });
 | |
|             const logs = await callBridgeTransferFromAsync({ bridgeData });
 | |
|             const btfs = filterLogsToArguments<BtfCalledEventArgs>(logs, TestEvents.BridgeTransferFromCalled);
 | |
|             for (const [call, btf] of shortZip(goodBridgeCalls, btfs)) {
 | |
|                 expect(btf.inputTokenBalance).to.bignumber.eq(call.inputTokenAmount);
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         it('transfers only up to outstanding sell amount to each bridge', async () => {
 | |
|             // Prepend an extra bridge call.
 | |
|             const calls = [
 | |
|                 await createBridgeCallAsync({
 | |
|                     callFields: {
 | |
|                         inputTokenAmount: new BigNumber(1),
 | |
|                         outputTokenAmount: new BigNumber(1),
 | |
|                     },
 | |
|                 }),
 | |
|                 ...goodBridgeCalls,
 | |
|             ];
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls,
 | |
|             });
 | |
|             const logs = await callBridgeTransferFromAsync({ bridgeData });
 | |
|             const btfs = filterLogsToArguments<BtfCalledEventArgs>(logs, TestEvents.BridgeTransferFromCalled);
 | |
|             expect(btfs).to.be.length(goodBridgeCalls.length + 1);
 | |
|             // The last call will receive 1 less token.
 | |
|             const lastCall = calls.slice(-1)[0];
 | |
|             const lastBtf = btfs.slice(-1)[0];
 | |
|             expect(lastBtf.inputTokenBalance).to.bignumber.eq(lastCall.inputTokenAmount.minus(1));
 | |
|         });
 | |
| 
 | |
|         it('recoups funds from a bridge that fails', async () => {
 | |
|             // Prepend a call that will take the whole input amount but will
 | |
|             // fail.
 | |
|             const badCall = await createBridgeCallAsync({
 | |
|                 callFields: { inputTokenAmount: totalFillableInputAmount },
 | |
|                 returnCode: BRIDGE_FAILURE,
 | |
|             });
 | |
|             const calls = [badCall, ...goodBridgeCalls];
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls,
 | |
|             });
 | |
|             const logs = await callBridgeTransferFromAsync({ bridgeData });
 | |
|             const btfs = filterLogsToArguments<BtfCalledEventArgs>(logs, TestEvents.BridgeTransferFromCalled);
 | |
|             expect(btfs).to.be.length(goodBridgeCalls.length);
 | |
|         });
 | |
| 
 | |
|         it('recoups funds from a bridge that reverts', async () => {
 | |
|             // Prepend a call that will take the whole input amount but will
 | |
|             // revert.
 | |
|             const badCall = await createBridgeCallAsync({
 | |
|                 callFields: { inputTokenAmount: totalFillableInputAmount },
 | |
|                 revertError: BRIDGE_REVERT_ERROR,
 | |
|             });
 | |
|             const calls = [badCall, ...goodBridgeCalls];
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls,
 | |
|             });
 | |
|             const logs = await callBridgeTransferFromAsync({ bridgeData });
 | |
|             const btfs = filterLogsToArguments<BtfCalledEventArgs>(logs, TestEvents.BridgeTransferFromCalled);
 | |
|             expect(btfs).to.be.length(goodBridgeCalls.length);
 | |
|         });
 | |
| 
 | |
|         it('recoups funds from a bridge that under-pays', async () => {
 | |
|             // Prepend a call that will take the whole input amount but will
 | |
|             // underpay the output amount..
 | |
|             const badCall = await createBridgeCallAsync({
 | |
|                 callFields: {
 | |
|                     inputTokenAmount: totalFillableInputAmount,
 | |
|                     outputTokenAmount: new BigNumber(2),
 | |
|                 },
 | |
|                 outputFillAmount: new BigNumber(1),
 | |
|             });
 | |
|             const calls = [badCall, ...goodBridgeCalls];
 | |
|             const bridgeData = dexForwarderBridgeDataEncoder.encode({
 | |
|                 inputToken,
 | |
|                 calls,
 | |
|             });
 | |
|             const logs = await callBridgeTransferFromAsync({ bridgeData });
 | |
|             const btfs = filterLogsToArguments<BtfCalledEventArgs>(logs, TestEvents.BridgeTransferFromCalled);
 | |
|             expect(btfs).to.be.length(goodBridgeCalls.length);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe('executeBridgeCall()', () => {
 | |
|         it('cannot be called externally', async () => {
 | |
|             return expect(
 | |
|                 testContract
 | |
|                     .executeBridgeCall(
 | |
|                         randomAddress(),
 | |
|                         randomAddress(),
 | |
|                         randomAddress(),
 | |
|                         randomAddress(),
 | |
|                         new BigNumber(1),
 | |
|                         new BigNumber(1),
 | |
|                         constants.NULL_BYTES,
 | |
|                     )
 | |
|                     .callAsync(),
 | |
|             ).to.revertWith('DexForwarderBridge/ONLY_SELF');
 | |
|         });
 | |
|     });
 | |
| });
 |