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');
 | 
						|
        });
 | 
						|
    });
 | 
						|
});
 |